Bases de la sécurité
"Failed to send a request to the Edge Function" : la vraie cause
"Failed to send a request to the Edge Function" : le navigateur n'a reçu aucune réponse lisible. Sa console dit pourquoi, et si votre fonction a tourné.

En bref
- "Failed to send a request to the Edge Function" signifie que supabase-js n'a reçu aucune réponse qu'il avait le droit de lire. Soit la requête a échoué en chemin, soit le navigateur a retenu la réponse, et votre app n'en apprend pas plus, exprès.
- La console du navigateur a la raison. Une ligne qui mentionne la preflight request veut dire que votre requête n'est jamais partie. Une ligne qui ne la mentionne pas veut dire que votre fonction a tourné et que seule sa réponse a été retenue.
- La correction : une branche OPTIONS tout en haut de la fonction, et les en-têtes CORS sur chaque réponse, erreurs comprises. Ces en-têtes disent seulement aux navigateurs qui peut lire la réponse ; qui peut appeler, c'est une vérification dans votre code qui le décide.
Votre app a un bouton qui appelle une Supabase Edge Function, ce petit morceau de code serveur qu'une app construite avec Lovable, Bolt, Cursor ou Claude Code utilise pour le travail qui ne peut pas se faire dans le navigateur, comme appeler OpenAI avec une clé. Vous appuyez dessus et, au lieu d'un résultat, votre app affiche ceci :
Failed to send a request to the Edge Function
La fonction est déployée. Appelée depuis un terminal, elle répond peut-être même parfaitement. Depuis votre app, elle échoue à chaque fois, et "failed to send a request to the Edge Function" donne l'impression que votre requête n'est jamais partie.
Elle l'est peut-être bel et bien. Votre fonction a pu tourner, faire son travail et répondre, et le navigateur a refusé de remettre la réponse à votre app. Lequel des deux s'est produit est écrit à un seul endroit, et ce n'est pas l'erreur que votre app a reçue.
Que veut dire "Failed to send a request to the Edge Function" ?
Cela veut dire que supabase-js a demandé une réponse et n'en a reçu aucune qu'il avait le droit de lire.
La phrase est un texte fixe dans
supabase-js,
attaché à une erreur appelée FunctionsFetchError. supabase-js la lève quand
la requête elle-même échoue, avant qu'il y ait le moindre code de statut à lire.
C'est ce qui la distingue de sa sœur plus connue :
Edge Function returned a non-2xx status code
veut dire qu'une réponse est revenue et que c'était une erreur. Celle-ci veut
dire qu'aucune réponse lisible n'est revenue du tout.
Une requête échoue ainsi pour une poignée de raisons. L'appareil est hors ligne, l'adresse du projet dans votre app est fausse, ou les règles du navigateur pour les autres adresses ont refusé la réponse. Ces règles s'appellent CORS, pour Cross-Origin Resource Sharing, et elles empêchent une page web de lire une réponse venue d'une autre adresse, sauf si la réponse l'autorise. Quand la fonction répond depuis un terminal et n'échoue que dans votre app, c'est CORS qu'il faut soupçonner, et le navigateur vous dira si c'était bien lui.
Votre app habite à une adresse, disons yourapp.lovable.app, et votre fonction
à une autre, <project-ref>.supabase.co. Voyez le navigateur comme le
concierge de votre app. Le courrier venu d'une autre adresse n'est remis à votre
app que s'il porte une étiquette de l'expéditeur disant que votre app peut
l'avoir : un en-tête de réponse appelé Access-Control-Allow-Origin. Pour une
lettre comme la vôtre, le concierge frappe d'abord à la porte de l'expéditeur
pour demander s'il l'accepte. Et quand il refuse quelque chose, il dit seulement
à votre app que ça n'a pas marché. La raison, il l'écrit dans son propre
registre.
Un troisième message fait partie de la même famille. Relay Error invoking the Edge Function, c'est supabase-js qui signale que le relais de Supabase n'a pas
pu joindre votre fonction, alors
vérifiez si Supabase a un problème avant
de toucher au code.
Pourquoi votre app n'en apprend pas plus
Parce que le navigateur cache exprès la raison à votre code.
La page de MDN sur les erreurs CORS
le dit clairement : pour des raisons de sécurité, le code JavaScript n'a pas
accès au détail de ce qui s'est mal passé dans une requête CORS. Votre app
apprend que quelque chose a échoué, et rien d'autre. Dans Chrome, l'erreur que
supabase-js enveloppe se lit TypeError: Failed to fetch, un texte que Chromium
utilise pour toutes les pannes réseau sans distinction, donc l'afficher
n'apporte rien. Selon la même page de MDN, le seul moyen de savoir ce qui s'est
passé est la console du navigateur.
C'est le registre du concierge. Ouvrez votre app en ligne dans Chrome ou Edge, appuyez sur F12, choisissez Console et appuyez de nouveau sur le bouton de votre app. La ligne que vous cherchez est rouge et commence ainsi :
Access to fetch at 'https://<project-ref>.supabase.co/functions/v1/<name>' from origin 'https://yourapp.lovable.app' has been blocked by CORS policy:
Tout ce qui suit les deux-points est la raison. Firefox et Safari le formulent autrement ; les citations de cet article sont celles de Chrome, tirées du code source de Chromium.
Le coup frappé à la porte a un nom technique, la preflight request. C'est une
requête avec la méthode OPTIONS que le navigateur envoie avant la vôtre, et
supabase-js en déclenche toujours une : il envoie votre clé dans un en-tête
Authorization et vos données en JSON, et le
guide CORS de MDN
ne compte ni l'un ni l'autre parmi ce qu'une page peut envoyer sans demander
d'abord.
Quelle moitié a échoué, et votre fonction a-t-elle tourné ?
Les mots "preflight request" dans la ligne de la console tranchent. S'ils y sont, le navigateur s'est arrêté au coup frappé à la porte et votre requête n'est jamais partie. S'ils n'y sont pas, votre requête est passée, votre fonction a tourné, et sa réponse a été retenue.
| La ligne de la console se termine par | Ce qui s'est passé | La fonction a-t-elle fait le travail ? |
|---|---|---|
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. | Le coup a reçu une réponse sans étiquette | Non. Seul le coup l'a atteinte |
Response to preflight request doesn't pass access control check: It does not have HTTP ok status. | Le coup a reçu une erreur pour réponse | Non |
Request header field <name> is not allowed by Access-Control-Allow-Headers in preflight response. | Votre app envoie un en-tête que la liste de la fonction ne nomme pas | Non |
No 'Access-Control-Allow-Origin' header is present on the requested resource., sans mention de preflight | Le coup est passé, votre requête est arrivée, la réponse n'avait pas d'étiquette | Oui |
| Aucune ligne CORS | Pas CORS : la connexion, l'adresse ou un délai fixé par votre code | Regardez l'onglet Invocations |
La première ligne est ce que produit une fonction sans gestionnaire
OPTIONS. La
page de Supabase sur l'erreur 500
le décrit du côté de la fonction : le coup atteint votre code comme n'importe
quelle requête, et la fonction écrit SyntaxError: Unexpected end of JSON input dans ses logs, parce qu'un coup à la porte n'a pas de contenu et que le
code a essayé d'en lire un. Sa réponse est une erreur sans en-têtes, et Chrome
signale l'en-tête manquant avant de regarder le statut. Si cette ligne est dans
l'onglet Logs de la fonction, vous avez trouvé.
La deuxième ligne veut dire que le coup a reçu les en-têtes, et une erreur
avec. Une façon d'y arriver est une vérification placée au-dessus de la
branche OPTIONS que le coup ne peut pas passer, comme une vérification de qui
appelle. Le coup ne porte aucun de vos en-têtes, votre clé comprise, donc il
échoue à chaque fois.
La quatrième ligne mérite qu'on ralentisse, parce que le travail a été fait. Si la fonction enregistre une ligne, envoie un e-mail ou débite une carte, c'est fait, et appuyer de nouveau sur le bouton le refait. Ce qui a échoué, c'est la réponse, partie sans étiquette. Cherchez d'abord une erreur : un plantage, dont votre code n'a jamais eu le temps d'écrire la réponse, ou une branche d'erreur qui renvoie sans les en-têtes. La même page de Supabase prévient que le navigateur accuse alors CORS d'un échec dont la vraie cause est ailleurs.
Lisez donc le propre journal de la fonction avant de toucher aux en-têtes. Dans le tableau de bord Supabase, ouvrez Edge Functions, choisissez la fonction, et lisez ses onglets Invocations et Logs à la minute où vous avez appuyé sur le bouton. Lire la vraie erreur passe en revue les deux, et ce que veut dire chaque code de statut.
La dernière ligne veut dire que le concierge n'a rien refusé. La requête a
échoué en chemin : l'appareil a perdu sa connexion, ou l'adresse de projet que
votre app appelle n'existe pas. Un cas vient de votre propre code. S'il passe un
timeout à supabase.functions.invoke,
supabase-js
annule la requête quand le temps est écoulé et signale cette même phrase, même
si la fonction est peut-être encore en train de travailler.
Comment corriger : répondre au coup, et étiqueter chaque réponse
Deux changements dans la fonction, et ils vont dans le même déploiement.
- Une branche
OPTIONStout au début, avant toute ligne qui lit le contenu ou vérifie qui appelle. Elle répond au coup avec les en-têtes CORS, et rien d'autre. - Les en-têtes CORS sur chaque réponse que la fonction renvoie, celles qui réussissent comme les erreurs. Une réponse sans eux est retenue, quoi qu'elle dise.
Le guide CORS de Supabase construit une fonction à peu près ainsi :
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 })
}
})
Les en-têtes viennent de supabase-js lui-même plutôt que d'une liste collée dans
la fonction. Le guide les importe ainsi depuis la version 2.95.0 de
supabase-js, pour que la liste corresponde à ce que les bibliothèques clientes
envoient, et il dit de redéployer après une mise à jour. Les fonctions plus
anciennes portent une copie écrite à la main de la même liste, dans un fichier
appelé _shared/cors.ts. C'est de cette copie que vient la troisième ligne du
tableau : un en-tête que votre app envoie et que la liste ne nomme pas.
Si vous demandez à Lovable, Bolt, Cursor ou Claude Code de faire la correction, ces deux points numérotés sont le prompt. Nommez les deux. Le second s'oublie facilement, et une fonction qui n'a que le premier passe de la première ligne du tableau à la quatrième au prochain plantage.
Les exemples récents de Supabase enveloppent toute la fonction dans
withSupabase, qui répond au coup pour vous.
Déplacer un secret dans une Edge Function
montre cette enveloppe avec une vérification de qui appelle, et c'est le sujet
de la section suivante.
Une fois les en-têtes sur chaque réponse, une fonction qui plante n'affiche plus
ce message mais Edge Function returned a non-2xx status code. La réponse
atteint maintenant votre app, avec la raison à l'intérieur.
L'en-tête CORS décide-t-il qui peut appeler votre fonction ?
Non. Il décide quelles pages web peuvent lire la réponse, et seuls les navigateurs le regardent.
La liste de Supabase elle-même met Access-Control-Allow-Origin à *, ce qui
autorise toutes les origines, aussi bien dans la copie qu'exporte supabase-js
que dans l'ancienne écrite à la main. Sur une fonction qui vérifie le jeton de
l'appelant, ça convient. Sur une fonction qui ne vérifie rien, cela permet à
n'importe quelle page de n'importe quel site d'appeler votre fonction depuis les
navigateurs de ses propres visiteurs et de lire ce qui revient.
Restreindre l'en-tête à votre propre domaine ne ferme pas non plus la fonction. Un script, un terminal ou un autre serveur envoie la même requête sans navigateur, ne lit jamais l'en-tête, et reçoit toute la réponse. Ce qu'un joker CORS ouvre ou pas le détaille.
La serrure d'une Edge Function, c'est une vérification de qui appelle, écrite dans la fonction. Elle compte surtout sur les fonctions qui finissent derrière cette erreur, parce que ce sont elles qui détiennent une clé OpenAI ou la clé secrète de votre projet, et la clé secrète passe outre chaque règle de row level security que vous avez écrite.
L'autre sortie d'une erreur CORS, c'est de ne plus utiliser la fonction : mettre
la clé dans une variable VITE_ et appeler OpenAI directement depuis la page.
Ça marche tout de suite, et ça met la clé dans le fichier que chaque visiteur
télécharge. C'est la première des
deux corrections qui vous laissent en plus mauvaise posture.
Cette erreur se voit de l'extérieur. Notre scan gratuit lit le JavaScript de
votre app en ligne comme le ferait un inconnu et nomme chaque clé qu'il trouve,
en disant si elle a sa place là. Cela prend environ 20 secondes, sans compte :
scannez votre app.
Garder la clé hors de la page après le prochain changement
Une fonction corrigée aujourd'hui peut être réécrite la prochaine fois que vous demandez une fonctionnalité à votre builder, et le moyen le plus rapide de contourner une erreur CORS est celui qui met votre clé dans la page.
Reeve Monitor rescanne votre app en ligne toutes les heures :
- les neuf vérifications toutes les heures, sur trois apps au plus
- un e-mail le jour où un nouveau scan note votre app moins bien que le précédent ; une clé secrète dans votre JavaScript plafonne à elle seule la note à D
- si l'app répond, toutes les 60 secondes
- un rapport mensuel de ce qui a été vu
Une fonction qui détient la clé secrète de votre projet peut modifier n'importe quelle ligne de votre base pour quiconque l'appelle. Reeve Care garde une copie de votre base Supabase là où rien dans votre projet ne peut l'atteindre.
- une copie chiffrée selon un calendrier, conservée sur le stockage de Reeve dans l'UE, en dehors de votre compte Supabase
- chaque copie vérifiée avant de compter, en comptant les lignes de chaque table
- une restauration en un clic qui sauvegarde ce qui est là avant de le remplacer
- vos fichiers téléversés aussi, dès que vous connectez un accès Storage
- tout ce que fait Monitor
Que faire maintenant
Que faire
- Ouvrez la console du navigateur sur votre app en ligne, appuyez de nouveau sur le bouton et lisez la fin de la ligne CORS rouge.
- Si la ligne mentionne la preflight request, ajoutez tout en haut de la fonction une branche
OPTIONSqui renvoie les en-têtes CORS. - Sinon, votre fonction a tourné. Lisez ses onglets Invocations et Logs, et vérifiez ce qu'elle a déjà fait avant que quiconque appuie de nouveau sur le bouton.
- Renvoyez les en-têtes CORS sur chaque réponse, erreurs comprises, et importez-les depuis supabase-js 2.95.0 ou plus récent pour que la liste reste à jour.
- Gardez dans la fonction une vérification de qui appelle. L'en-tête CORS dit seulement aux navigateurs qui peut lire la réponse.
Quand l'erreur dans votre app devient Edge Function returned a non-2xx status code, les en-têtes marchent et la réponse passe ;
cet article la lit à
partir de là. La checklist de sécurité en 10 minutes couvre ce
qu'une app qui vient d'être lancée laisse souvent ouvert.
FAQ
Que veut dire "Failed to send a request to the Edge Function" ?
Cela veut dire que supabase-js a demandé une réponse à votre Supabase Edge Function et n'en a reçu aucune qu'il avait le droit de lire. Soit la requête a échoué en chemin, par exemple parce que l'appareil était hors ligne, soit le navigateur a refusé de remettre la réponse à votre app, selon ses règles sur les requêtes vers d'autres adresses, appelées CORS. Le navigateur cache exprès la raison à votre code et l'écrit dans sa console.
Pourquoi mon Edge Function marche depuis un terminal mais pas depuis mon app ?
Parce que seuls les navigateurs appliquent CORS. Un terminal, un script ou un autre serveur envoie la requête et lit toute la réponse. Un navigateur vérifie d'abord que la réponse autorise votre app à la lire, et avant une requête comme celle que supabase-js envoie, il demande en plus la permission à la fonction par une requête OPTIONS séparée. Si l'une des deux étapes échoue, votre app reçoit cette erreur alors que le même appel marche partout ailleurs.
Ma Edge Function a-t-elle tourné si j'ai cette erreur ?
Lisez la ligne rouge dans la console du navigateur. Si elle mentionne la preflight request, le navigateur s'est arrêté à la demande de permission et votre requête n'est jamais partie. Sinon, la requête est passée, la fonction a fait son travail et seule la réponse a été retenue. Dans ce cas, lisez les onglets Invocations et Logs de la fonction avant d'appuyer à nouveau sur le bouton, car un second clic refait le travail.
Comment corriger une erreur CORS sur une Supabase Edge Function ?
Faites répondre la fonction à la requête OPTIONS en premier, avant tout code qui lit le contenu ou vérifie l'appelant, et renvoyez les en-têtes CORS sur chaque réponse, réponses d'erreur comprises. Le guide CORS de Supabase importe ces en-têtes depuis supabase-js, version 2.95.0 ou plus récente, pour que la liste corresponde à ce que le client envoie. Redéployez la fonction après la modification.
Qu'est-ce que "Relay Error invoking the Edge Function" ?
C'est un troisième message de supabase-js, levé quand le relais de Supabase n'a pas pu joindre votre fonction. Ce n'est ni un refus CORS ni une réponse de votre propre code, alors vérifiez d'abord si Supabase lui-même a un problème avant de modifier quoi que ce soit.
Est-ce sûr d'autoriser toutes les origines dans les en-têtes CORS de ma Edge Function ?
Seulement si la fonction vérifie qui l'appelle. L'en-tête CORS dit aux navigateurs quelles pages web peuvent lire la réponse ; il n'empêche personne d'appeler la fonction. Les en-têtes de Supabase autorisent toutes les origines, ce qui convient à une fonction qui vérifie le jeton de l'appelant et laisse n'importe quel site utiliser une fonction qui ne le fait pas. Restreindre l'en-tête à votre domaine ne ferme pas non plus la fonction aux scripts.