Zum Inhalt springen

Sicherheitsgrundlagen

"Edge Function returned a non-2xx status code": der echte Fehler

"Edge Function returned a non-2xx status code" heißt: Deine Supabase-Funktion hat mit einem Fehler geantwortet. So liest du den echten.

Vlad Tkachenko11 Min. Lesezeit
Ein zurückgeschickter Umschlag mit einem gelben Stempel quer über der Vorderseite und offener Lasche, aus der der gefaltete Brief noch herausschaut.

Kurz gesagt

  • "Edge Function returned a non-2xx status code" sagt supabase-js zu jeder Antwort deiner Funktion außerhalb der 200er. Der echte Grund kam in derselben Antwort zurück, und die Logs der Funktion enthalten den Rest.
  • Lies zuerst die Statusnummer. 401 heißt, die Anfrage wurde abgewiesen, bevor dein Code lief, 404, dass keine Funktion dieses Namens deployt ist, 500, dass dein Code gescheitert ist, 546 oder 504, dass Ressourcen oder Zeit ausgingen.
  • Zwei schnelle Lösungen lassen die Meldung verschwinden und reißen ein Loch: die API aus deinem Frontend aufrufen, mit dem Schlüssel darin, und die JWT-Prüfung abschalten, ohne eine eigene Prüfung an ihre Stelle zu setzen.

Du drückst in deiner App den Knopf, der eine Edge Function aufruft, und statt eines Ergebnisses zeigt deine App das hier:

Edge Function returned a non-2xx status code

Es ist derselbe Satz, egal was schiefging. Ein fehlender Schlüssel, eine abgestürzte Funktion, eine Anfrage, die abgewiesen wurde, bevor dein Code lief: Alle lauten "Edge Function returned a non-2xx status code", also sagt dir die Meldung allein fast nichts darüber, was zu reparieren ist.

Was sie dir nicht sagt: Der Grund ist schon angekommen. Der Fehler kam in derselben Antwort zurück wie dieser Satz, und supabase-js liest ihn dir nicht vor. Wer den Satz sucht, findet die Vermutungen anderer Leute. Wer die Antwort liest, findet seine eigene.

Was bedeutet "Edge Function returned a non-2xx status code"?

Deine Funktion hat geantwortet, und die Antwort war ein Fehler. Mehr sagt der Satz nicht.

Jede Antwort im Web trägt einen Statuscode, eine dreistellige Zahl, die sagt, wie die Anfrage gelaufen ist. Alles in den 200ern heißt, es hat geklappt. Wenn deine App eine Funktion über supabase.functions.invoke aufruft, schaut supabase-js auf diese Zahl, und liegt sie außerhalb der 200er, bekommt deine App statt der Daten einen festen Satz. Der Satz steht wörtlich in supabase-js selbst, und der Fehler, der ihn trägt, heißt FunctionsHttpError.

Stell dir den Aufruf als Brief vor, den du an ein Büro im Hinterhaus schickst. Das Büro beantwortet entweder, was du gefragt hast, oder schickt den Brief zurück. Kommt er zurück, liest supabase-js den Stempel auf dem Umschlag und sagt dir, dass er zurückkam. Den Umschlag öffnet es nicht. Die Notiz darin sagt, warum, und das Büro hat ein Logbuch über alles geführt, was mit deinem Brief passiert ist.

Zwei nahe Verwandte sehen ähnlich aus und bedeuten etwas anderes. Failed to send a request to the Edge Function heißt, dass überhaupt keine lesbare Antwort zurückkam, und im Browser ist das oft ein fehlender CORS-Header, den der Beitrag zu dieser Meldung behandelt. Und wenn mehrere voneinander unabhängige Teile deiner App im selben Moment kaputtgingen, prüf zuerst, ob es an Supabase oder an deiner App liegt, bevor du irgendeinen Code öffnest.

Die Antwort trägt den Status und den Grund. supabase-js gibt deiner App einen Satz weiter, und der Grund hängt weiter am Fehler und steht im Log der Funktion.

Wo steht der echte Fehler?

An zwei Stellen, und du kannst beide lesen, ohne eine Zeile Code zu ändern.

Die Antwort selbst. Öffne deine Live-App in Chrome, Edge oder Firefox, drück F12, wähl den Tab Network und wiederhole, was fehlgeschlagen ist. Klick die Anfrage an, deren Adresse /functions/v1/ gefolgt vom Namen deiner Funktion enthält. Ihre Spalte Status zeigt die Zahl, und ihr Tab Response zeigt, was die Funktion mitgeschickt hat: eine Meldung, die dein eigener Code geschrieben hat, ein kurzer JSON-Inhalt von der Plattform von Supabase oder die Worte Internal Server Error.

Wenn du oder das Werkzeug, das deinen Code schreibt, den Aufruf ändern kann, zeigt der Leitfaden zur Fehlerbehandlung von Supabase, wie man dieselbe Antwort in der App liest, denn die ursprüngliche Antwort reist mit dem Fehler als error.context:

const { data, error } = await supabase.functions.invoke('your-function', { body })
if (error) console.log(error.context.status, await error.context.text())

Das Logbuch der Funktion. Öffne im Supabase-Dashboard Edge Functions, wähl die Funktion und lies zwei Tabs. Invocations listet jede Anfrage mit ihrem Statuscode und ihrer Dauer. Logs enthält, was der Logging-Leitfaden von Supabase Plattform-Ereignisse, nicht abgefangene Ausnahmen und eigene Log-Meldungen nennt, also alles, was dein Code mit console.log oder console.error ausgegeben hat. Lös den Fehler einmal aus, merk dir die Uhrzeit und lies beide Tabs zu dieser Minute.

Lies zuerst den Statuscode

Die Zahl sagt dir, wie weit deine Anfrage kam, bevor sie zurückgeschickt wurde, und das entscheidet, wo der Grund aufgeschrieben ist.

StatusWas passiert istWo der Grund steht
401Abgewiesen, bevor dein Code lief, oder dein Code hat abgewiesenDie Antwort. Invalid JWT oder Missing authorization header ist die Plattform
404In diesem Projekt ist keine Funktion mit diesem Namen deploytDie Antwort lautet Requested function was not found
500Dein Code lief und hat einen Fehler geworfen oder absichtlich 500 geliefertDer Tab Logs
503Die Funktion ist gar nicht erst gestartetDer Tab Logs: ein Import, ein Syntaxfehler oder die Konfiguration der Funktion
504Keine Antwort innerhalb von 150 SekundenDer Tab Invocations, dann das, worauf dein Code gewartet hat
546CPU-Zeit oder Speicher sind ausgegangenDer Tab Logs sagt, welches von beiden
Jeder andere 4xxDein eigener Code hat ihn gewählt, etwa indem er die Antwort eines Dienstes weitergibtDie Antwort

Die Bedeutungen stammen aus der Statuscode-Referenz von Supabase und ihrer Seite zu den Limits. Für einen 404 reicht diese Tabelle: Vergleich den Namen der Funktion in deiner App mit der Liste im Dashboard, und prüf, ob sie in dem Projekt deployt ist, mit dem deine App spricht. Die anderen Zeilen haben unten je einen eigenen Abschnitt.

500: Deine Funktion lief und ist gescheitert

Irgendetwas in deinem Code hat einen Fehler geworfen, und der Tab Logs hat die Zeile.

Die Seite von Supabase zu diesem Fehler teilt ihn in zwei Fälle. Lautet die Antwort genau Internal Server Error, ist dein Code abgestürzt, und das Log zeigt die Art des Fehlers und die Zeile, in der er passiert ist, etwa TypeError: Cannot read properties of undefined. Lautet die Antwort anders, hat dein Code diese Meldung selbst geschrieben und die 500 absichtlich geliefert, also such in der Funktion nach dem Status, den sie geschickt hat.

Eine Ursache lohnt es vor allen anderen auszuschließen, wenn eine Funktion OpenAI, Stripe oder irgendeine bezahlte API aufruft: ein Secret, das auf deinem Rechner existiert und in deinem Live-Projekt nicht.

In der Funktion wird ein Secret mit Deno.env.get('OPENAI_API_KEY') gelesen. Ist kein Secret dieses Namens gesetzt, liefert das laut Doku von Deno undefined, statt zu scheitern. Die Funktion läuft also weiter, baut eine Anfrage mit dem Wort undefined an der Stelle des Schlüssels und schickt sie ab. OpenAI antwortet mit einem eigenen 401, "Incorrect API key provided", und deine Funktion gibt das entweder weiter oder stürzt beim Lesen einer Antwort ab, mit der sie nicht gerechnet hat. Im Bild mit dem Umschlag: Das Büro hat deinen Brief geöffnet, ist zu der Schublade gegangen, in der der Schlüssel liegen sollte, hat sie leer vorgefunden und eine Notiz über einen falschen Schlüssel zurückgeschickt.

Die Reparatur liegt auf der Seite Edge Function Secrets deines Supabase-Projekts. Laut Secrets-Leitfaden von Supabase liest eine Funktion ein neues Secret sofort, du musst also nicht neu deployen, und kein Secret-Name darf mit SUPABASE_ beginnen, weil dieses Präfix für die Werte reserviert ist, die Supabase für dich setzt. Ein Secret, das du für die lokale Entwicklung setzt, bleibt auf deinem Rechner; das Live-Projekt hat seine eigene Liste.

401: Abgewiesen, bevor dein Code lief

Lautet die Antwort Invalid JWT oder Missing authorization header, hat deine Funktion die Anfrage nie gesehen. Die Plattform von Supabase hat sie an der Tür abgewiesen.

Diese Tür ist eine Prüfung namens verify_jwt, standardmäßig an, die den Header Authorization jeder Anfrage liest, bevor dein Code läuft. Die Seite von Supabase zum 401 nennt drei Arten, an ihr zu scheitern:

  • Der Schlüssel passt nicht zu den Schlüsseln deines Projekts. Zum Beispiel ein Schlüssel aus einem anderen Projekt oder ein alter, der nach einer Rotation noch irgendwo eingetragen ist.
  • Es gibt überhaupt keinen Header Authorization. supabase-js fügt einen für dich hinzu. Ein einfaches fetch an die Adresse der Funktion oder ein Webhook von einem anderen Dienst tut das nicht.
  • Dein Projekt nutzt die neueren Signaturschlüssel von Supabase. Die eingebaute Prüfung versteht nur das ältere Format, und die Lösung von Supabase ist, die Prüfung für diese Funktion abzuschalten.

Dieser letzte Punkt reicht über den einen Fall hinaus. Supabase empfiehlt inzwischen, die eingebaute Prüfung abzuschalten und die Authentifizierung direkt im Code deiner Funktion zu erledigen. Die Empfehlung hat zwei Hälften, und die zweite ist die, die unter den Tisch fällt.

Kam der 401 mit einer Meldung, die dein eigener Code geschrieben hat, hat die Plattform die Anfrage durchgelassen, und deine Funktion hat sie abgewiesen oder einen 401 des aufgerufenen Dienstes weitergegeben, oft das leere Secret aus dem Abschnitt zur 500. Das ist außerdem eine andere Prüfung als die hinter PGRST301 bei deinen Datenbankabfragen, die eine eigene Seite hat. Steht im Log der Funktion invalid claim: missing sub claim, hat sie Supabase Auth gefragt, wer da aufruft, und statt eines angemeldeten Nutzers den Schlüssel deines Projekts bekommen; der Beitrag zu dieser Meldung verfolgt das.

Der Status sagt, wie weit die Anfrage kam. Ein 401 am Tor hat deinen Code nie erreicht; ein 401, den der aufgerufene Dienst zurückgibt, schon.

546 und 504: keine Ressourcen oder keine Zeit mehr

Supabase stoppt eine Funktion, die zu viel braucht, und die beiden Zahlen sagen, welches Limit sie getroffen hat.

546 ist Rechenleistung. Die Seite zu den Limits gibt einer Funktion 256 MB Speicher und jeder Anfrage 2 Sekunden CPU-Zeit. Geht eines davon darüber, lautet die Antwort Function failed due to not having enough compute resources (please check logs), mit dem Code WORKER_RESOURCE_LIMIT (ältere Logs sagen WORKER_LIMIT). Der Tab Logs sagt CPU Time exceeded oder Memory limit exceeded. Die Seite von Supabase zum 546 nennt Bildverarbeitung, KI-Embeddings und Web-Scraping als Beispiele, und ihre Antwort ist, pro Anfrage weniger zu tun oder den schweren Teil an einen anderen Dienst abzugeben.

504 ist Zeit. Eine Funktion, die innerhalb von 150 Sekunden keine Antwort schickt, bekommt einen 504. Das CPU-Limit zählt nur die Zeit, in der dein Code tatsächlich rechnet: Laut der Seite zu den Limits zählt asynchrone Ein- und Ausgabe nicht dazu, und genau das ist Warten auf das Netz. Eine Funktion, die auf ein langsames KI-Modell wartet, verbraucht also keine CPU. Sie lässt die Uhr von 150 Sekunden ablaufen, und wenn die abgelaufen ist, bekommst du einen 504 und keinen 546.

Zwei Lösungen, die es schlimmer machen

Zwei Änderungen lassen diese Meldung in wenigen Minuten verschwinden, und beide lassen dich schlechter dastehen als der Fehler.

Die API stattdessen aus der Seite aufrufen. Am schnellsten hört eine fehlschlagende Funktion auf zu fehlschlagen, wenn man sie nicht mehr benutzt: den Schlüssel in eine Variable mit VITE_ legen und OpenAI direkt aus deinem Frontend aufrufen. Das klappt beim ersten Versuch. Es legt den Schlüssel aber auch in die Datei, die jeder Besucher herunterlädt, denn dieses Präfix ist eine Anweisung, den Wert in dein Bundle zu kompilieren, und dort kann ihn jeder lesen. Unter den Live-Apps, die wir gescannt haben, tauchte im JavaScript von 33 ein OpenAI-Schlüssel auf, und was ein Fremder damit anfangen kann, steht in einem eigenen Beitrag.

Die Prüfung abschalten und dabei stehen bleiben. Einen 401 zu beseitigen, indem man verify_jwt abschaltet, ist die erste Hälfte des eigenen Rats von Supabase. Der Konfigurationsleitfaden von Supabase sagt klar, was das allein bewirkt: Dann kann jeder deine Edge Function ohne gültiges JWT aufrufen. Die Adresse deiner Funktion steht in deinem Frontend, für jeden lesbar. Hält die Funktion einen OpenAI-Schlüssel, kann ein Fremder ihn jetzt über dich ausgeben. Nutzt sie den geheimen Schlüssel des Projekts, den Supabase in die Umgebung jeder Funktion legt, läuft die Anfrage eines Fremden mit einem Schlüssel, der an jeder Row-Level-Security-Regel vorbeikommt, die du geschrieben hast.

Die zweite Hälfte, eine Prüfung im Code der Funktion, wer da aufruft, ist ein paar Zeilen lang. Ein Secret in eine Edge Function verschieben enthält sie und erklärt, warum selbst die Standardprüfung mehr Aufrufer durchlässt, als ihr Name vermuten lässt.

Den ersten Fehler sieht man von außen, und so haben wir ihn gezählt. Unser kostenloser Scan liest das JavaScript deiner Live-App so, wie ein Fremder es lesen würde, und nennt jeden Schlüssel, den er findet, samt der Angabe, ob er dorthin gehört. Das dauert etwa 20 Sekunden und braucht kein Konto: scanne deine App.

Damit der Schlüssel bleibt, wo du ihn hingelegt hast

Die Lösung von heute kann der Prompt von nächster Woche rückgängig machen. Ein Builder, der ein Feature wieder zum Laufen bringen soll, darf die Abkürzung von oben nehmen, und deine App läuft weiter, während er es tut.

Reeve Monitor prüft jede Stunde, damit du es nicht musst:

  • 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 mit deinem geheimen Schlüssel läuft, kommt an jeder Row-Level-Security-Regel vorbei. 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

  • Lös den Fehler einmal aus und merk dir die Uhrzeit.
  • Öffne die Anfrage im Tab Network deines Browsers und lies Status und Antwort, bevor du irgendetwas änderst.
  • Lies die Tabs Invocations und Logs der Funktion im Supabase-Dashboard zu dieser Minute.
  • Bei einer 500 aus einer Funktion, die eine bezahlte API aufruft: Prüf, ob das Secret auf der Seite Edge Function Secrets des Live-Projekts gesetzt ist. Danach ist kein neues Deploy nötig.
  • Bei einem 401 von der Plattform: Korrigier den Schlüssel oder schalte die eingebaute Prüfung ab, und füg in derselben Änderung eine Prüfung in der Funktion hinzu, wer da aufruft.
  • Beseitige diesen Fehler nie, indem du den Schlüssel in eine Variable mit VITE_ verschiebst.

Die Prüfung, wer da aufruft, ist der Schritt, den man am leichtesten auslässt, und die Anleitung zu Secrets in Edge Functions behandelt ihn vollständig. Die 10-Minuten-Sicherheitscheckliste deckt ab, was in einer frisch gestarteten App sonst gern offen bleibt.

FAQ

Was bedeutet "Edge Function returned a non-2xx status code"?

Deine Supabase Edge Function hat mit einem Fehlerstatus geantwortet, also mit irgendeiner Zahl außerhalb der 200er, und supabase-js hat aus dieser Antwort einen festen Satz gemacht. Der Satz ist derselbe für einen Absturz, einen fehlenden Schlüssel, eine Anfrage, die abgewiesen wurde, bevor dein Code lief, und eine Zeitüberschreitung. Die Antwort selbst, die sagt, was davon es war, hängt noch am Fehler, und die Funktion führt ein eigenes Log.

Wie sehe ich den echten Fehler dahinter?

Öffne deine Live-App, drück F12, wähl den Tab Network und wiederhole, was fehlgeschlagen ist. Klick die Anfrage an, deren Adresse /functions/v1/ enthält, und lies ihren Status und ihre Antwort: Das ist, was die Funktion tatsächlich geschickt hat. Öffne dann Edge Functions in deinem Supabase-Dashboard, wähl die Funktion und lies zur selben Zeit die Tabs Invocations und Logs. Im Code liest der Leitfaden von Supabase dieselbe Antwort mit await error.context.json().

Warum läuft meine Edge Function lokal und scheitert nach dem Deploy?

Prüf zuerst, ob ein Secret auf deinem Rechner existiert und in deinem Live-Projekt nicht. Die lokale Umgebung liest ihre eigene Umgebungsdatei, die deployte Funktion liest die Secrets, die im Projekt gesetzt sind. Ein fehlendes Secret kommt als undefined zurück statt als Fehler, also läuft die Funktion weiter und scheitert später, meist wenn der aufgerufene Dienst einen leeren Schlüssel ablehnt. Setz es auf der Seite Edge Function Secrets; laut Supabase wird ein neues Secret sofort gelesen, ohne neues Deploy.

Soll ich die JWT-Prüfung abschalten, um einen 401 loszuwerden?

Supabase empfiehlt inzwischen, die eingebaute Prüfung abzuschalten und den Aufrufer stattdessen in deiner Funktion zu prüfen. Beide Hälften zählen. Allein abgeschaltet hält die Prüfung nichts mehr auf, und die Doku von Supabase warnt selbst, dass dann jeder deine Funktion ohne gültiges JWT aufrufen kann. Hält die Funktion einen OpenAI-Schlüssel oder den geheimen Schlüssel des Projekts, ist das jeder im Internet, der dein Kontingent verbraucht oder an deinen Row-Level-Security-Regeln vorbeikommt.

Was bedeutet Fehler 546 bei einer Supabase Edge Function?

Der Funktion ist die Rechenleistung ausgegangen: mehr als 2 Sekunden CPU-Zeit oder mehr als ihr Speicher bei einer Anfrage. Die Antwort lautet "Function failed due to not having enough compute resources (please check logs)", und der Tab Logs sagt, welches Limit es war, CPU Time exceeded oder Memory limit exceeded. Das Warten auf eine langsame API zählt nicht als CPU-Zeit; eine Funktion, die zu lange wartet, bekommt stattdessen einen 504.

Geschrieben von

Vlad Tkachenko

Gründer, Reeve

Ich schaue mir den ganzen Tag Apps an, die mit Lovable, Bolt, v0, Cursor und Replit gebaut wurden, und die kurze Liste von Fehlern, die darin immer wieder auftauchen.

Mehr über den Autor

Weiterlesen

Alle Artikel

Sichere deine Supabase-Datenbank vor der nächsten Änderung

Reeve sichert sie verschlüsselt nach Zeitplan und stellt sie für dich wieder her, wenn etwas schiefgeht.

Supabase-Backups ansehen

Automatisierte externe Prüfung, kein vollständiges Audit. Das Fehlen von Funden ist keine Garantie für Sicherheit.