Noções de segurança
"Failed to send a request to the Edge Function": a causa real
"Failed to send a request to the Edge Function": o navegador não recebeu uma resposta legível. O console dele diz por quê e se a sua função rodou.

Em resumo
- "Failed to send a request to the Edge Function" quer dizer que o supabase-js não recebeu nenhuma resposta que tivesse permissão de ler. Ou a requisição falhou no caminho, ou o navegador segurou a resposta, e o seu app não fica sabendo de mais nada de propósito.
- O console do navegador tem o motivo. Uma linha que menciona a preflight request quer dizer que a sua requisição nunca saiu. Uma linha que não menciona quer dizer que a sua função rodou e só a resposta dela ficou retida.
- A correção é um ramo para OPTIONS no topo da função e os cabeçalhos CORS em toda resposta, erros incluídos. Esses cabeçalhos só dizem aos navegadores quem pode ler a resposta; quem pode chamar quem decide é uma verificação no seu código.
O seu app tem um botão que chama uma Supabase Edge Function, aquele pedacinho de código de servidor que um app feito com Lovable, Bolt, Cursor ou Claude Code usa para o trabalho que não pode acontecer no navegador, como chamar a OpenAI com uma chave. Você aperta, e em vez de um resultado o seu app mostra isto:
Failed to send a request to the Edge Function
A função está no ar. Chamada pelo terminal, ela pode até responder direitinho. Pelo seu app, falha toda vez, e "failed to send a request to the Edge Function" dá a entender que a sua requisição nunca saiu.
Pode muito bem ter saído. A sua função pode ter rodado, feito o trabalho e respondido, e o navegador se recusou a entregar a resposta ao seu app. Qual das duas coisas aconteceu está escrito num lugar só, e não é o erro que o seu app recebeu.
O que significa "Failed to send a request to the Edge Function"?
Significa que o supabase-js pediu uma resposta e não recebeu nenhuma que tivesse permissão de ler.
A frase é um texto fixo dentro do
supabase-js,
preso a um erro chamado FunctionsFetchError. O supabase-js lança esse erro
quando a própria requisição falha, antes de existir qualquer código de status
para olhar. É isso que o separa do irmão mais conhecido:
Edge Function returned a non-2xx status code
quer dizer que uma resposta voltou e era um erro. Este quer dizer que nenhuma
resposta legível voltou.
Uma requisição falha assim por um punhado de motivos. O aparelho está sem conexão, o endereço do projeto no seu app está errado, ou as regras do navegador para outros endereços recusaram a resposta. Essas regras se chamam CORS, de Cross-Origin Resource Sharing, e impedem que uma página web leia uma resposta de outro endereço, a não ser que a resposta permita. Quando a função responde pelo terminal e só falha no seu app, o suspeito é o CORS, e o navegador vai dizer se foi ele.
O seu app mora num endereço, digamos yourapp.lovable.app, e a sua função em
outro, <project-ref>.supabase.co. Pense no navegador como o porteiro do seu
app. A correspondência que chega de outro endereço só é entregue ao seu app se
trouxer uma etiqueta do remetente dizendo que o seu app pode ficar com ela: um
cabeçalho de resposta chamado Access-Control-Allow-Origin. Com uma carta como a
sua, o porteiro primeiro bate à porta do remetente para perguntar se ele aceita.
E quando recusa alguma coisa, só diz ao seu app que não deu certo. O motivo ele
anota no próprio caderno.
Uma terceira mensagem é da mesma família. Relay Error invoking the Edge Function é o supabase-js avisando que o relay do Supabase não conseguiu alcançar
a sua função, então
confira se o Supabase está com problema
antes de mexer no código.
Por que o seu app não fica sabendo de mais nada
Porque o navegador esconde o motivo do seu código de propósito.
A página da MDN sobre erros de CORS
diz isso com todas as letras: por motivos de segurança, o código JavaScript não
tem acesso aos detalhes do que deu errado numa requisição CORS. O seu app fica
sabendo que algo falhou e mais nada. No Chrome, o erro que o supabase-js embrulha
diz TypeError: Failed to fetch, um texto que o Chromium usa igual para
qualquer falha de rede, então imprimir esse erro não acrescenta nada. Segundo a
mesma página da MDN, o único jeito de descobrir o que aconteceu é o console do
navegador.
Esse é o caderno do porteiro. Abra o seu app publicado no Chrome ou no Edge, aperte F12, escolha Console e aperte de novo o botão no seu app. A linha que você procura é vermelha e começa assim:
Access to fetch at 'https://<project-ref>.supabase.co/functions/v1/<name>' from origin 'https://yourapp.lovable.app' has been blocked by CORS policy:
Tudo o que vem depois dos dois-pontos é o motivo. O Firefox e o Safari escrevem de outro jeito; as citações deste post são as do Chrome, tiradas do código-fonte do Chromium.
A batida na porta tem um nome técnico, preflight request. É uma requisição com o
método OPTIONS que o navegador envia antes da sua, e o supabase-js sempre
dispara uma: ele manda a sua chave num cabeçalho Authorization e os seus dados
como JSON, e o
guia de CORS da MDN
não inclui nenhuma das duas coisas no que uma página pode enviar sem perguntar
antes.
Qual metade falhou, e a sua função chegou a rodar?
As palavras "preflight request" na linha do console decidem. Se elas estão lá, o navegador parou na batida e a sua requisição nunca foi enviada. Se não estão, a sua requisição passou, a sua função rodou e a resposta dela ficou retida.
| A linha do console termina com | O que aconteceu | A função fez o trabalho? |
|---|---|---|
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. | A batida foi respondida sem etiqueta | Não. Só a batida chegou a ela |
Response to preflight request doesn't pass access control check: It does not have HTTP ok status. | A batida foi respondida com um erro | Não |
Request header field <name> is not allowed by Access-Control-Allow-Headers in preflight response. | O seu app envia um cabeçalho que a lista da função não nomeia | Não |
No 'Access-Control-Allow-Origin' header is present on the requested resource., sem mencionar preflight | A batida passou, a sua requisição chegou e a resposta veio sem etiqueta | Sim |
| Nenhuma linha de CORS | Não é CORS: a conexão, o endereço ou um tempo limite do seu código | Olhe a aba Invocations |
A primeira linha é o que uma função sem tratamento para OPTIONS
produz. A
página do Supabase sobre o erro 500
descreve isso do lado da função: a batida chega ao seu código como qualquer
outra requisição, e a função registra SyntaxError: Unexpected end of JSON input, porque uma batida na porta não traz conteúdo e o código tentou ler um. A
resposta dela é um erro sem cabeçalhos, e o Chrome aponta o cabeçalho que falta
antes de olhar o status. Se essa linha está na aba Logs da função, você achou.
A segunda linha quer dizer que a batida recebeu os cabeçalhos, e um erro junto
com eles. Um jeito de chegar aí é uma verificação colocada acima do ramo de
OPTIONS que a batida não consegue passar, como uma verificação de quem chama.
A batida não leva nenhum dos seus cabeçalhos, nem a sua chave, então ela falha
toda vez.
A quarta linha é a que pede calma, porque o trabalho foi feito. Se a função grava uma linha, manda um e-mail ou cobra um cartão, isso já aconteceu, e apertar o botão de novo faz tudo outra vez. O que falhou foi a resposta, que saiu sem etiqueta. Procure primeiro um erro: uma quebra cuja resposta o seu código nem chegou a escrever, ou um ramo de erro que devolve sem os cabeçalhos. A mesma página do Supabase avisa que, nesse caso, o navegador culpa o CORS por uma falha cuja causa real é outra.
Então leia o registro da própria função antes de mexer em qualquer cabeçalho. No painel do Supabase, abra Edge Functions, escolha a função e leia as abas Invocations e Logs no minuto em que você apertou o botão. Ler o erro real percorre as duas, e o que cada código de status quer dizer.
A última linha quer dizer que o porteiro não recusou nada. A requisição
falhou no caminho: o aparelho perdeu a conexão, ou o endereço de projeto que o
seu app chama não existe. Um caso vem do seu próprio código. Se ele passa um
timeout para supabase.functions.invoke, o
supabase-js
cancela a requisição quando o tempo acaba e informa esta mesma frase, mesmo que
a função talvez ainda esteja trabalhando.
Como corrigir: responder à batida e etiquetar toda resposta
Duas mudanças na função, e elas vão no mesmo deploy.
- Um ramo para
OPTIONSantes de tudo, antes de qualquer linha que leia o conteúdo ou verifique quem chama. Ele responde à batida com os cabeçalhos CORS e mais nada. - Os cabeçalhos CORS em toda resposta que a função devolve, as que dão certo e os erros. Uma resposta sem eles fica retida, diga o que disser.
O guia de CORS do Supabase monta uma função mais ou menos assim:
import { corsHeaders } from 'npm:@supabase/supabase-js@^2/cors'
Deno.serve(async (req) => {
if (req.method === 'OPTIONS') {
return Response.json({ ok: true }, { headers: corsHeaders })
}
try {
const body = await req.json()
// the function's own work goes here
return Response.json({ done: true }, { headers: corsHeaders })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return Response.json({ error: message }, { status: 400, headers: corsHeaders })
}
})
Os cabeçalhos vêm do próprio supabase-js em vez de uma lista colada na função. O
guia importa os cabeçalhos assim a partir da versão 2.95.0 do supabase-js, para
que a lista bata com o que as bibliotecas cliente enviam, e diz para fazer o
deploy de novo depois de atualizar. Funções mais antigas trazem uma cópia escrita
à mão da mesma lista, num arquivo chamado _shared/cors.ts. É dessa cópia que
vem a terceira linha da tabela: um cabeçalho que o seu app envia e que a lista
não nomeia.
Se você pedir a correção ao Lovable, ao Bolt, ao Cursor ou ao Claude Code, esses dois pontos numerados são o prompt. Cite os dois. O segundo é fácil de esquecer, e uma função que só tem o primeiro passa da primeira linha da tabela para a quarta na próxima vez que quebrar.
Os exemplos mais novos do Supabase envolvem a função inteira em withSupabase,
que responde à batida por você.
Levar um segredo para uma Edge Function
mostra esse envoltório com uma verificação de quem chama, e essa verificação é o
assunto da próxima seção.
Com os cabeçalhos em toda resposta, uma função que quebra para de mostrar esta
mensagem e passa a mostrar Edge Function returned a non-2xx status code. A
resposta agora chega ao seu app, com o motivo dentro.
O cabeçalho CORS decide quem pode chamar a sua função?
Não. Ele decide quais páginas web podem ler a resposta, e só navegadores olham para ele.
A própria lista do Supabase define Access-Control-Allow-Origin como *, que
libera qualquer origem, tanto na cópia que o supabase-js exporta quanto na antiga
escrita à mão. Numa função que verifica o token de quem chama, tudo bem. Numa
função que não verifica nada, isso deixa qualquer página de qualquer site chamar
a sua função a partir dos navegadores dos próprios visitantes e ler o que volta.
Restringir o cabeçalho ao seu próprio domínio também não fecha a função. Um script, um terminal ou outro servidor envia a mesma requisição sem navegador, nunca lê o cabeçalho e recebe a resposta inteira. O que um curinga de CORS abre e o que não abre explica isso em detalhe.
A tranca de uma Edge Function é uma verificação de quem chama, escrita dentro da função. Ela importa mais nas funções que acabam atrás deste erro, porque são elas que guardam uma chave da OpenAI ou a chave secreta do seu projeto, e a chave secreta passa por cima de todas as regras de row level security que você escreveu.
A outra saída de um erro de CORS é parar de usar a função: colocar a chave numa
variável VITE_ e chamar a OpenAI direto da página. Funciona na hora, e coloca a
chave no arquivo que todo visitante baixa. É a primeira das
duas correções que deixam você pior.
Esse erro dá para ver de fora. A nossa varredura gratuita lê o JavaScript do seu
app publicado como um estranho leria e nomeia cada chave que encontra, dizendo
inclusive se ela devia estar ali. Leva uns 20 segundos e não precisa de conta:
escaneie o seu app.
Manter a chave fora da página depois da próxima mudança
Uma função que você corrigiu hoje pode ser reescrita na próxima vez que você pedir uma funcionalidade ao seu builder, e o jeito mais rápido de passar por um erro de CORS é o que coloca a sua chave na página.
O Reeve Monitor varre o seu app publicado de novo a cada hora:
- as nove verificações a cada hora, em até três apps
- um e-mail no dia em que uma nova varredura avaliar o seu app pior que a anterior; uma chave secreta no seu JavaScript limita sozinha a nota a D
- se o app está no ar, a cada 60 segundos
- um relatório mensal do que ele viu
Uma função que guarda a chave secreta do seu projeto pode mudar qualquer linha do seu banco para quem a chamar. O Reeve Care guarda uma cópia do seu banco Supabase onde nada no seu projeto alcança.
- uma cópia criptografada conforme uma agenda, guardada no armazenamento do próprio Reeve na UE, fora da sua conta Supabase
- cada cópia verificada antes de contar, contando as linhas de cada tabela
- uma restauração em um clique que salva o que está lá agora antes de substituir
- também os seus arquivos enviados, assim que você conectar uma credencial de Storage
- tudo o que o Monitor faz
O que fazer agora
O que fazer
- Abra o console do navegador no seu app publicado, aperte o botão de novo e leia o fim da linha vermelha de CORS.
- Se a linha menciona a preflight request, coloque no topo da função um ramo para
OPTIONSque devolva os cabeçalhos CORS. - Se não menciona, a sua função rodou. Leia as abas Invocations e Logs dela e confira o que ela já fez antes que alguém aperte o botão de novo.
- Devolva os cabeçalhos CORS em toda resposta, erros incluídos, e importe-os do supabase-js 2.95.0 ou mais novo para a lista ficar em dia.
- Mantenha dentro da função uma verificação de quem chama. O cabeçalho CORS só diz aos navegadores quem pode ler a resposta.
Quando o erro no seu app mudar para Edge Function returned a non-2xx status code, os cabeçalhos estão funcionando e a resposta está passando;
esse post lê a resposta a
partir daí. O checklist de segurança de 10 minutos cobre o que mais
um app recém-lançado costuma deixar aberto.
Perguntas frequentes
O que significa "Failed to send a request to the Edge Function"?
Significa que o supabase-js pediu uma resposta à sua Supabase Edge Function e não recebeu nenhuma que tivesse permissão de ler. Ou a requisição falhou no caminho, por exemplo porque o aparelho estava sem conexão, ou o navegador se recusou a entregar a resposta ao seu app pelas regras dele para requisições a outros endereços, chamadas CORS. O navegador esconde o motivo do seu código de propósito e o escreve no console.
Por que a minha Edge Function funciona pelo terminal mas não pelo meu app?
Porque só navegadores aplicam CORS. Um terminal, um script ou outro servidor envia a requisição e lê a resposta inteira. Um navegador confere antes se a resposta diz que o seu app pode lê-la, e antes de uma requisição como a que o supabase-js envia ele ainda pede permissão à função com uma requisição OPTIONS separada. Se qualquer um dos dois passos falhar, o seu app recebe este erro enquanto a mesma chamada funciona em qualquer outro lugar.
A minha Edge Function rodou se eu recebi este erro?
Leia a linha vermelha no console do navegador. Se ela menciona a preflight request, o navegador parou no pedido de permissão e a sua requisição nunca saiu. Se não menciona, a requisição passou, a função fez o trabalho dela e só a resposta ficou retida. Nesse caso, leia as abas Invocations e Logs da função antes de apertar o botão de novo, porque um segundo clique repete o trabalho.
Como corrijo um erro de CORS numa Supabase Edge Function?
Faça a função responder primeiro à requisição OPTIONS, antes de qualquer código que leia o conteúdo ou verifique quem chama, e devolva os cabeçalhos CORS em toda resposta, inclusive nas de erro. O guia de CORS do Supabase importa esses cabeçalhos do supabase-js, versão 2.95.0 ou mais nova, para que a lista bata com o que o cliente envia. Faça o deploy da função de novo depois da mudança.
O que é "Relay Error invoking the Edge Function"?
É uma terceira mensagem do supabase-js, que aparece quando o relay do Supabase não conseguiu alcançar a sua função. Não é uma recusa de CORS nem uma resposta do seu próprio código, então confira primeiro se o próprio Supabase está com problema antes de mudar qualquer coisa.
É seguro liberar qualquer origem nos cabeçalhos CORS da minha Edge Function?
Só se a função verificar quem a chama. O cabeçalho CORS diz aos navegadores quais páginas web podem ler a resposta; ele não impede ninguém de chamar a função. Os cabeçalhos do próprio Supabase liberam qualquer origem, o que é tranquilo para uma função que verifica o token de quem chama e deixa qualquer site usar uma que não verifica. Restringir o cabeçalho ao seu domínio também não fecha a função para scripts.