Sicherheitsgrundlagen
"Failed to send a request to the Edge Function": der echte Grund
"Failed to send a request to the Edge Function" heißt: Der Browser hat keine lesbare Antwort bekommen. Seine Konsole sagt, warum, und ob deine Funktion lief.

Kurz gesagt
- "Failed to send a request to the Edge Function" heißt, dass supabase-js keine Antwort bekommen hat, die es lesen durfte. Entweder ist die Anfrage unterwegs gescheitert, oder der Browser hat die Antwort zurückgehalten, und deine App erfährt mit Absicht nicht mehr.
- Die Konsole des Browsers kennt den Grund. Eine Zeile, die den Preflight Request erwähnt, heißt: Deine Anfrage wurde nie abgeschickt. Eine Zeile ohne ihn heißt: Deine Funktion lief, und nur ihre Antwort wurde zurückgehalten.
- Die Lösung ist ein Zweig für OPTIONS ganz oben in der Funktion und CORS-Header an jeder Antwort, auch an Fehlern. Diese Header sagen Browsern nur, wer die Antwort lesen darf; wer aufrufen darf, entscheidet eine Prüfung in deinem Code.
Deine App hat einen Knopf, der eine Supabase Edge Function aufruft, das kleine Stück Servercode, das eine mit Lovable, Bolt, Cursor oder Claude Code gebaute App für Arbeit benutzt, die nicht im Browser passieren kann, etwa einen Aufruf bei OpenAI mit einem Schlüssel. Du drückst ihn, und statt eines Ergebnisses zeigt deine App das hier:
Failed to send a request to the Edge Function
Die Funktion ist deployt. Aus einem Terminal aufgerufen, antwortet sie vielleicht sogar tadellos. Aus deiner App scheitert sie jedes Mal, und "failed to send a request to the Edge Function" liest sich, als hätte deine Anfrage die App nie verlassen.
Gut möglich, dass sie es doch hat. Deine Funktion ist vielleicht gelaufen, hat ihre Arbeit getan und geantwortet, und der Browser hat sich geweigert, die Antwort an deine App weiterzugeben. Was von beidem passiert ist, steht an genau einer Stelle, und das ist nicht der Fehler, den deine App bekommen hat.
Was bedeutet "Failed to send a request to the Edge Function"?
Es heißt, dass supabase-js um eine Antwort gebeten und keine bekommen hat, die es lesen durfte.
Der Satz steht als fester Text in
supabase-js
und hängt an einem Fehler namens FunctionsFetchError. supabase-js wirft ihn,
wenn die Anfrage selbst scheitert, bevor es überhaupt einen Statuscode zum
Ansehen gibt. Das trennt ihn von seinem bekannteren Geschwister:
Edge Function returned a non-2xx status code
heißt, dass eine Antwort zurückkam und sie ein Fehler war. Dieser hier heißt,
dass überhaupt keine lesbare Antwort zurückkam.
Eine Anfrage scheitert so aus einer Handvoll Gründen. Das Gerät ist offline, die Projektadresse in deiner App ist falsch, oder die Regeln des Browsers für fremde Adressen haben die Antwort abgelehnt. Diese Regeln heißen CORS, kurz für Cross-Origin Resource Sharing, und sie verhindern, dass eine Webseite eine Antwort von einer anderen Adresse liest, außer die Antwort erlaubt es. Wenn die Funktion im Terminal antwortet und nur in deiner App scheitert, ist CORS der Verdächtige, und der Browser sagt dir, ob er es war.
Deine App wohnt unter einer Adresse, sagen wir yourapp.lovable.app, und deine
Funktion unter einer anderen, <project-ref>.supabase.co. Stell dir den Browser
als Portier deiner App vor. Post von einer anderen Adresse bekommt deine App nur,
wenn sie ein Etikett des Absenders trägt, auf dem steht, dass deine App sie haben
darf: einen Antwort-Header namens Access-Control-Allow-Origin. Bei einem Brief
wie deinem klopft der Portier vorher beim Absender an und fragt, ob der ihn
überhaupt annimmt. Und wenn er etwas ablehnt, sagt er deiner App nur, dass es
nicht geklappt hat. Den Grund schreibt er in sein eigenes Buch.
Eine dritte Meldung gehört zur selben Familie. Relay Error invoking the Edge Function ist supabase-js, das meldet, dass das Relay von Supabase deine Funktion
nicht erreicht hat, also
prüf, ob Supabase gerade ein Problem hat,
bevor du irgendeinen Code änderst.
Warum deine App nicht mehr erfährt
Weil der Browser den Grund mit Absicht vor deinem Code zurückhält.
Die Seite von MDN zu CORS-Fehlern
sagt es klar: Aus Sicherheitsgründen bekommt JavaScript-Code keine Einzelheiten
darüber, was bei einer CORS-Anfrage schiefgelaufen ist. Deine App erfährt, dass
etwas gescheitert ist, und sonst nichts. In Chrome lautet der Fehler, den
supabase-js einpackt, TypeError: Failed to fetch, und Chromium benutzt diesen
Text für jeden Netzwerkfehler gleich, also bringt es nichts, ihn auszugeben. Laut
derselben Seite von MDN findest du nur in der Konsole des Browsers heraus, was
genau schiefging.
Das ist das Buch des Portiers. Öffne deine Live-App in Chrome oder Edge, drück F12, wähl Console und drück den Knopf in deiner App noch einmal. Die Zeile, die du suchst, ist rot und beginnt so:
Access to fetch at 'https://<project-ref>.supabase.co/functions/v1/<name>' from origin 'https://yourapp.lovable.app' has been blocked by CORS policy:
Alles nach dem Doppelpunkt ist der Grund. Firefox und Safari formulieren es anders; die Zitate in diesem Beitrag sind die von Chrome, aus dem Quellcode von Chromium.
Das Anklopfen hat einen technischen Namen, Preflight Request. Es ist eine
Anfrage mit der Methode OPTIONS, die der Browser vor deiner schickt, und
supabase-js löst immer eine aus: Es schickt deinen Schlüssel in einem Header
Authorization und deine Daten als JSON, und der
CORS-Leitfaden von MDN
zählt keins von beiden zu dem, was eine Seite schicken darf, ohne vorher zu
fragen.
Welche Hälfte ist gescheitert, und lief deine Funktion?
Die Worte "preflight request" in der Konsolenzeile entscheiden es. Stehen sie da, hat der Browser beim Anklopfen aufgehört, und deine Anfrage wurde nie abgeschickt. Stehen sie nicht da, ist deine Anfrage durchgekommen, deine Funktion lief, und ihre Antwort wurde zurückgehalten.
| Die Konsolenzeile endet mit | Was passiert ist | Hat die Funktion die Arbeit getan? |
|---|---|---|
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. | Das Anklopfen wurde ohne Etikett beantwortet | Nein. Nur das Anklopfen kam an |
Response to preflight request doesn't pass access control check: It does not have HTTP ok status. | Das Anklopfen wurde mit einem Fehler beantwortet | Nein |
Request header field <name> is not allowed by Access-Control-Allow-Headers in preflight response. | Deine App schickt einen Header, den die Liste der Funktion nicht nennt | Nein |
No 'Access-Control-Allow-Origin' header is present on the requested resource., ohne Erwähnung von preflight | Das Anklopfen kam durch, deine Anfrage kam an, die Antwort hatte kein Etikett | Ja |
| Gar keine CORS-Zeile | Nicht CORS: die Verbindung, die Adresse oder ein Timeout aus deinem Code | Sieh im Tab Invocations nach |
Die erste Zeile ist das, was eine Funktion ohne Handler für OPTIONS
erzeugt. Die
Seite von Supabase zum 500-Fehler
beschreibt es von der Seite der Funktion: Das Anklopfen erreicht deinen Code wie
jede andere Anfrage, und die Funktion schreibt SyntaxError: Unexpected end of JSON input ins Log, weil ein Anklopfen keinen Inhalt hat und der Code einen
lesen wollte. Ihre Antwort ist ein Fehler ohne Header, und Chrome meldet den
fehlenden Header, bevor es auf den Status schaut. Steht diese Zeile im Tab Logs
der Funktion, hast du es gefunden.
Die zweite Zeile heißt, dass das Anklopfen die Header bekam und einen Fehler
dazu. Ein Weg dorthin ist eine Prüfung oberhalb des Zweigs für OPTIONS, die
das Anklopfen nicht bestehen kann, etwa eine Prüfung, wer da aufruft. Das
Anklopfen trägt keinen deiner Header, auch nicht deinen Schlüssel, also fällt es
jedes Mal durch.
Bei der vierten Zeile lohnt es sich, langsamer zu werden, denn die Arbeit ist passiert. Speichert die Funktion eine Zeile, verschickt eine E-Mail oder belastet eine Karte, dann ist das geschehen, und ein zweiter Druck auf den Knopf tut es noch einmal. Gescheitert ist die Antwort, die ohne Etikett hinausging. Such zuerst nach einem Fehler: einem Absturz, dessen Antwort dein Code gar nicht mehr schreiben konnte, oder einem Fehlerzweig, der ohne die Header zurückgibt. Dieselbe Seite von Supabase warnt, dass der Browser dann CORS für ein Scheitern verantwortlich macht, dessen echter Grund ein anderer ist.
Lies also das eigene Protokoll der Funktion, bevor du irgendwelche Header anfasst. Öffne im Supabase-Dashboard Edge Functions, wähl die Funktion und lies ihre Tabs Invocations und Logs zu der Minute, in der du den Knopf gedrückt hast. Den echten Fehler lesen geht beide durch, und dazu, was jede Statusnummer bedeutet.
Die letzte Zeile heißt, dass der Portier nichts abgelehnt hat. Die Anfrage
ist unterwegs gescheitert: Das Gerät hat die Verbindung verloren, oder die
Projektadresse, die deine App aufruft, existiert nicht. Ein Fall kommt aus
deinem eigenen Code. Übergibt er ein timeout an supabase.functions.invoke,
bricht
supabase-js
die Anfrage ab, wenn die Zeit um ist, und meldet denselben Satz, auch wenn die
Funktion vielleicht noch arbeitet.
So behebst du es: das Anklopfen beantworten und jede Antwort etikettieren
Zwei Änderungen in der Funktion, und sie gehören ins selbe Deploy.
- Ein Zweig für
OPTIONSals allererstes, vor jeder Zeile, die den Inhalt liest oder prüft, wer da aufruft. Er beantwortet das Anklopfen mit den CORS-Headern und mit nichts sonst. - Die CORS-Header an jeder Antwort, die die Funktion zurückgibt, den erfolgreichen und den Fehlern. Eine Antwort ohne sie wird zurückgehalten, egal, was darin steht.
Der CORS-Leitfaden von Supabase baut eine Funktion ungefähr so auf:
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 })
}
})
Die Header kommen aus supabase-js selbst statt aus einer Liste, die in die
Funktion kopiert wurde. Der Leitfaden importiert sie so ab supabase-js Version
2.95.0, damit die Liste zu dem passt, was die Client-Bibliotheken schicken, und
sagt, dass man nach einem Upgrade neu deployen soll. Ältere Funktionen tragen eine
von Hand geschriebene Kopie derselben Liste in einer Datei namens
_shared/cors.ts. Aus dieser Kopie stammt die dritte Zeile der Tabelle: ein
Header, den deine App schickt und den die Liste nicht nennt.
Wenn du Lovable, Bolt, Cursor oder Claude Code bittest, das zu beheben, sind diese zwei nummerierten Punkte der Prompt. Nenn beide. Der zweite wird leicht vergessen, und eine Funktion, die nur den ersten hat, rutscht beim nächsten Absturz von der ersten Zeile der Tabelle in die vierte.
Die neueren Beispiele von Supabase packen die ganze Funktion in withSupabase,
das das Anklopfen für dich beantwortet.
Ein Secret in eine Edge Function verlegen
zeigt diese Hülle zusammen mit einer Prüfung, wer da aufruft, und um diese
Prüfung geht es im nächsten Abschnitt.
Sobald die Header an jeder Antwort hängen, zeigt eine Funktion, die abstürzt,
nicht mehr diese Meldung, sondern Edge Function returned a non-2xx status code.
Die Antwort erreicht jetzt deine App, mit dem Grund darin.
Entscheidet der CORS-Header, wer deine Funktion aufrufen darf?
Nein. Er entscheidet, welche Webseiten die Antwort lesen dürfen, und nur Browser schauen ihn überhaupt an.
Die eigene Liste von Supabase setzt Access-Control-Allow-Origin auf *, was
jeden Ursprung erlaubt, sowohl in der Kopie, die supabase-js exportiert, als auch
in der älteren von Hand geschriebenen. Bei einer Funktion, die das Token des
Aufrufers prüft, ist das in Ordnung. Bei einer Funktion, die nichts prüft, kann
damit jede Seite auf jeder Website deine Funktion aus den Browsern ihrer eigenen
Besucher aufrufen und lesen, was zurückkommt.
Den Header auf deine eigene Domain zu beschränken, verschließt die Funktion auch nicht. Ein Skript, ein Terminal oder ein anderer Server schickt dieselbe Anfrage ohne Browser, liest den Header nie und bekommt die ganze Antwort. Was eine CORS-Wildcard öffnet und was nicht geht das im Detail durch.
Das Schloss an einer Edge Function ist eine Prüfung, wer da aufruft, in die Funktion hineingeschrieben. Am meisten zählt sie bei den Funktionen, die hinter diesem Fehler landen, denn das sind die, die einen OpenAI-Schlüssel oder den geheimen Schlüssel deines Projekts halten, und der geheime Schlüssel kommt an jeder Row-Level-Security-Regel vorbei, die du geschrieben hast.
Der andere Ausweg aus einem CORS-Fehler ist, die Funktion nicht mehr zu benutzen:
den Schlüssel in eine Variable mit VITE_ legen und OpenAI direkt aus der Seite
aufrufen. Das klappt sofort, und es legt den Schlüssel in die Datei, die jeder
Besucher herunterlädt. Es ist die erste der
zwei Lösungen, nach denen du schlechter dastehst.
Diesen Fehler sieht man von außen. Unser kostenloser Scan liest das JavaScript
deiner Live-App so, wie ein Fremder es täte, und nennt jeden Schlüssel, den er
findet, samt der Frage, ob er dorthin gehört. Das dauert etwa 20 Sekunden und
braucht kein Konto: scanne deine App.
Damit der Schlüssel nach der nächsten Änderung nicht in der Seite landet
Eine Funktion, die du heute repariert hast, kann beim nächsten Feature, um das du deinen Builder bittest, neu geschrieben werden, und der schnellste Weg an einem CORS-Fehler vorbei ist der, der deinen Schlüssel in die Seite legt.
Reeve Monitor scannt deine Live-App jede Stunde neu:
- alle neun Checks jede Stunde, für bis zu drei Apps
- eine E-Mail an dem Tag, an dem ein erneuter Scan deine App schlechter benotet als der vorige; ein geheimer Schlüssel in deinem JavaScript deckelt die Note allein schon bei D
- ob die App erreichbar ist, alle 60 Sekunden
- ein monatlicher Bericht darüber, was gesehen wurde
Eine Funktion, die den geheimen Schlüssel deines Projekts hält, kann für jeden, der sie aufruft, jede Zeile deiner Datenbank ändern. Reeve Care hält eine Kopie deiner Supabase-Datenbank dort, wo nichts in deinem Projekt sie erreicht.
- eine verschlüsselte Kopie nach Zeitplan, auf Reeves eigenem Speicher in der EU, außerhalb deines Supabase-Kontos
- jede Kopie wird geprüft, bevor sie zählt, durch Zählen der Zeilen in jeder Tabelle
- eine Wiederherstellung per Klick, die sichert, was jetzt da ist, bevor sie es ersetzt
- deine hochgeladenen Dateien ebenfalls, sobald du eine Storage-Zugangsberechtigung verbindest
- alles, was Monitor tut
Was jetzt zu tun ist
Was zu tun ist
- Öffne die Konsole des Browsers auf deiner Live-App, drück den Knopf noch einmal und lies das Ende der roten CORS-Zeile.
- Erwähnt die Zeile den Preflight Request, füg ganz oben in der Funktion einen Zweig für
OPTIONSein, der die CORS-Header zurückgibt. - Erwähnt sie ihn nicht, ist deine Funktion gelaufen. Lies ihre Tabs Invocations und Logs und prüf, was sie schon getan hat, bevor irgendwer den Knopf noch einmal drückt.
- Gib die CORS-Header bei jeder Antwort zurück, auch bei Fehlern, und importiere sie aus supabase-js 2.95.0 oder neuer, damit die Liste aktuell bleibt.
- Behalte eine Prüfung, wer da aufruft, in der Funktion. Der CORS-Header sagt Browsern nur, wer die Antwort lesen darf.
Wenn der Fehler in deiner App zu Edge Function returned a non-2xx status code
wechselt, funktionieren die Header, und die Antwort kommt durch;
dieser Beitrag liest sie
von dort aus weiter. Die 10-Minuten-Sicherheitscheckliste deckt ab,
was in einer frisch gestarteten App sonst gern offen bleibt.
FAQ
Was bedeutet "Failed to send a request to the Edge Function"?
Es heißt, dass supabase-js deine Supabase Edge Function um eine Antwort gebeten und keine bekommen hat, die es lesen durfte. Entweder ist die Anfrage unterwegs gescheitert, etwa weil das Gerät offline war, oder der Browser hat sich geweigert, die Antwort an deine App zu geben, nach seinen Regeln für Anfragen an fremde Adressen, kurz CORS. Den Grund hält der Browser mit Absicht vor deinem Code zurück und schreibt ihn stattdessen in seine Konsole.
Warum funktioniert meine Edge Function im Terminal, aber nicht in meiner App?
Weil nur Browser CORS anwenden. Ein Terminal, ein Skript oder ein anderer Server schickt die Anfrage ab und liest die ganze Antwort. Ein Browser prüft vorher, ob die Antwort sagt, dass deine App sie lesen darf, und vor einer Anfrage wie der von supabase-js fragt er die Funktion zusätzlich mit einer eigenen OPTIONS-Anfrage um Erlaubnis. Scheitert einer der beiden Schritte, bekommt deine App diesen Fehler, während derselbe Aufruf überall sonst klappt.
Ist meine Edge Function gelaufen, wenn ich diesen Fehler bekomme?
Lies die rote Zeile in der Konsole des Browsers. Erwähnt sie den Preflight Request, hat der Browser bei der Erlaubnisfrage aufgehört, und deine Anfrage wurde nie abgeschickt. Erwähnt sie ihn nicht, ist die Anfrage durchgekommen, die Funktion hat ihre Arbeit getan, und nur die Antwort wurde zurückgehalten. Dann lies die Tabs Invocations und Logs der Funktion, bevor du den Knopf noch einmal drückst, denn ein zweiter Druck wiederholt die Arbeit.
Wie behebe ich einen CORS-Fehler bei einer Supabase Edge Function?
Lass die Funktion zuerst die OPTIONS-Anfrage beantworten, vor jedem Code, der den Inhalt liest oder den Aufrufer prüft, und gib die CORS-Header bei jeder Antwort mit, auch bei Fehlern. Der CORS-Leitfaden von Supabase importiert diese Header aus supabase-js, ab Version 2.95.0, damit die Liste zu dem passt, was der Client schickt. Deploye die Funktion nach der Änderung neu.
Was ist "Relay Error invoking the Edge Function"?
Das ist eine dritte Meldung von supabase-js, die kommt, wenn das Relay von Supabase deine Funktion nicht erreichen konnte. Sie ist weder eine Ablehnung durch CORS noch eine Antwort deines eigenen Codes, also prüf zuerst, ob Supabase selbst gerade ein Problem hat, bevor du irgendetwas änderst.
Ist es sicher, in den CORS-Headern meiner Edge Function jeden Ursprung zu erlauben?
Nur wenn die Funktion prüft, wer sie aufruft. Der CORS-Header sagt Browsern, welche Webseiten die Antwort lesen dürfen; er hält niemanden davon ab, die Funktion aufzurufen. Die eigenen Header von Supabase erlauben jeden Ursprung. Das ist in Ordnung für eine Funktion, die das Token des Aufrufers prüft, und lässt jede Website eine Funktion benutzen, die das nicht tut. Den Header auf deine Domain zu beschränken, verschließt die Funktion auch nicht vor Skripten.