Zum Inhalt springen

Sicherheitsgrundlagen

Supabase "invalid input syntax for type uuid": was im Zitat steht

Supabase "invalid input syntax for type uuid" heißt: Deine App hat etwas geschickt, das keine ID ist. Zitat lesen, Abfrage zurückhalten, Spalte behalten.

Vlad Tkachenko9 Min. Lesezeit
Ein Paketetikett mit einem Codefeld aus fünf Kästchen, davor ein Anhänger mit undefined in Bernstein, zu lang, um hineinzupassen.

Kurz gesagt

  • Supabase "invalid input syntax for type uuid" (Postgres-Code 22P02, HTTP 400 in deiner App) heißt: Eine uuid-Spalte hat einen Wert bekommen, der keine uuid ist. Der Text in Anführungszeichen ist genau das, was deine App geschickt hat.
  • "undefined" in den Anführungszeichen heißt: Die Abfrage lief, bevor es die ID gab, denn supabase-js schreibt einen fehlenden Wert als genau dieses Wort in die Anfrage. Halte die Abfrage zurück, bis die ID da ist.
  • Lass die Spalte eine uuid. Wer sie auf text umstellt, macht aus einem lauten Fehler eine Seite, die still nichts anzeigt.

Du öffnest das Dashboard deiner App, und die Liste der Projekte bleibt einen Moment leer und füllt sich dann. Oder sie bleibt leer. So oder so steht in der Browserkonsole eine rote Zeile, und die Anfrage dahinter hat so geantwortet:

{
  "code": "22P02",
  "details": null,
  "hint": null,
  "message": "invalid input syntax for type uuid: \"undefined\""
}

Supabase "invalid input syntax for type uuid" heißt: Postgres hat einen Wert bekommen, der keine uuid ist, für eine Spalte, die nur uuids enthält, und hat die ganze Anfrage abgelehnt. Der Teil in Anführungszeichen ist das, was deine App geschickt hat. Hier ist das das Wort undefined: Die Abfrage lief, bevor es die ID gab, die sie brauchte.

Die schnellen Lösungen, die für diesen Fehler kursieren, lauten: die Spalte auf text umstellen oder den Wert casten. Beide lassen die Meldung verschwinden. Keine von beiden gibt der Abfrage ihre ID, also fragt die Seite weiter nach dem Falschen und sagt es dir nicht mehr.

Was bedeutet "invalid input syntax for type uuid"?

Ein Wert auf dem Weg in eine uuid-Spalte war nicht so geschrieben, wie man eine uuid schreibt, also hat Postgres angehalten, bevor es irgendetwas ausgeführt hat. Keine Zeile wurde gelesen und keine geändert.

Eine uuid ist eine ID in einer festen Form: 32 Zeichen von 0 bis 9 und a bis f, geschrieben in Gruppen von 8, 4, 4, 4 und 12, zum Beispiel 3f2b8c1e-9a4d-4e7b-b1c2-5d6e7f8a9b0c. Supabase gibt jedem angemeldeten Nutzer so eine ID, und eine Spalte, die festhält, welche Zeile wem gehört, etwa user_id, enthält dieselbe Art von Wert. Postgres' Leser für diesen Typ akzeptiert "a series of 32 hexadecimal digits with an optional dash after each group of 4 hexadecimal digits, and optionally surrounded by {}". Alles andere endet mit dieser Meldung, und die Eingabe wird zitiert.

Stell dir die Spalte als Sortiermaschine vor, die nur eine Art Etikett lesen kann: einen Code in diesen fünf Gruppen. Deine App hat ein Paket aufs Band gelegt, auf dem ein Wort steht, wo der Code hingehört. Die Maschine hat das Band angehalten und dir das Wort vorgelesen.

FeldBeispielWas es dir sagt
code22P02Postgres' Code für diesen Fehler, in seiner Liste der Codes invalid_text_representation genannt
messageinvalid input syntax for type uuid: "undefined"Den Typ, den Postgres erwartet hat, und den genauen Wert, den es bekommen hat
details, hintnullMehr sagt Postgres zu diesem Fehler nicht
HTTP-Status400 Bad RequestSo antwortet PostgREST, der Teil von Supabase, der supabase.from(...)-Aufrufe in SQL übersetzt, auf jeden Code ohne eigenen Status (sein Code)

Lies, was in den Anführungszeichen steht

Der zitierte Wert ist die nützliche Hälfte der Meldung. Er ist genau das, was in der Datenbank angekommen ist, und jede Form, die er annimmt, zeigt auf eine andere Stelle deiner App.

In den AnführungszeichenWas deine App geschickt hatWo du suchen solltest
"undefined"eine Variable, die noch keinen Wert hatteeine Abfrage, die lief, bevor der Nutzer oder die ID der Seite geladen war; die nächsten zwei Abschnitte
""einen leeren Stringein Wert, der im Zustand deiner App mit '' beginnt, oder ein leer gelassenes Formularfeld
"null"null, an .eq() übergeben.eq() schreibt auch null als Wort. Um Zeilen zu finden, in denen die Spalte leer ist, sagt die Dokumentation von supabase-js "use .is() instead"
"[object Object]"ein ganzes Objekt, wo seine ID hingehörteuser statt user.id übergeben. Diesen Text schreibt JavaScript für jedes Objekt, das in einen String verwandelt wird
"user_…"eine ID von einem anderen Anmeldedienst als dem von SupabaseDie IDs von Clerk beginnen mit user_ und sind keine uuids. Der Abschnitt übers Umstellen der Spalte behandelt diesen Fall
eine Zahl oder ein Nameeinen Teil der Seitenadresseeine URL wie /projects/42 oder /projects/my-first-app, als ID gelesen

Kommt der Fehler bei jedem Besuch, such nach einem Namen, unter dem nichts steht. Kommt er nur beim ersten Aufbau der Seite, such nach einem Wert, der noch nicht geladen war.

Warum schickt supabase-js das Wort undefined?

Weil es jeden Filter als Text in die Adresse der Anfrage schreibt und JavaScript einen fehlenden Wert als das Wort undefined schreibt.

Ein Aufruf wie .eq('user_id', user?.id) wird zu ?user_id=eq. und dahinter dem Wert. Der Quellcode der Bibliothek baut das aus einer Vorlage, `eq.${value}`, und ein Kommentar weiter unten in derselben Datei sagt, was das mit einem fehlenden Wert macht: Er würde "show up as ?column=eq.undefined". PostgREST liest alles nach eq. als reinen Text, und sein Parser behandelt null nur nach is. als Schlüsselwort. Postgres bekommt die neun Buchstaben von undefined und versucht, sie als uuid zu lesen.

.rpc() geht den umgekehrten Weg. Seine Argumente reisen als JSON, und JSON lässt einen undefined-Wert ganz weg. Ein Funktionsaufruf verliert also das Argument, statt das Wort zu schicken, und Supabase antwortet mit Could not find the function.

supabase-js setzt den Wert des Filters als Text in die Anfrage. Ein fehlender Wert kommt als das Wort undefined an, und die uuid-Spalte lehnt ihn ab.

In einer React-App suchst du den fehlenden Wert an drei Stellen:

  • Der Nutzer ist noch nicht geladen. Supabase liest die angemeldete Sitzung asynchron (await supabase.auth.getSession()). Beim ersten Aufbau deiner Seite ist ein user im Zustand also noch leer und user?.id ist undefined. Eine Abfrage, die bei diesem ersten Aufbau startet, schickt genau das.
  • Die ID hat einen anderen Namen. Eine Route wie /projects/:projectId legt den Wert unter projectId ab, und eine Seite, die id liest, bekommt bei jedem Besuch undefined.
  • Die ID kommt aus Daten, die noch unterwegs sind. Eine ID aus dem Ergebnis einer anderen Abfrage, gelesen, bevor diese Abfrage geantwortet hat.

Halte die Abfrage zurück, bis es die ID gibt

Prüf vor dem Aufruf, ob die ID da ist, und lass die Seite ihren Ladezustand zeigen, bis sie ankommt. Für "undefined" und "" ist das die ganze Lösung.

useEffect(() => {
  if (!user?.id) return

  supabase
    .from('projects')
    .select('*')
    .eq('user_id', user.id)
    .then(({ data, error }) => {
      if (!error) setProjects(data)
    })
}, [user?.id])

Das return schickt nichts, solange die ID fehlt, und user?.id in der Liste am Ende lässt den Effect erneut laufen, sobald die ID auftaucht. An der Sortiermaschine heißt das: warten, bis der Code auf dem Etikett steht, bevor das Paket aufs Band kommt.

Wenn deine App mit TanStack Query lädt (dem useQuery-Hook), ist die Option enabled derselbe Riegel. Seine Anleitung zu abhängigen Abfragen setzt enabled: !!userId mit dem Hinweis "The query will not execute until the userId exists".

const { data: projects } = useQuery({
  queryKey: ['projects', user?.id],
  queryFn: async () => {
    const { data, error } = await supabase
      .from('projects')
      .select('*')
      .eq('user_id', user.id)
    if (error) throw error
    return data
  },
  enabled: !!user?.id,
})

Wenn du per Prompt baust, bitte in einem Satz darum. Lass ihn auf Englisch, denn in dieser Sprache denken diese Werkzeuge: "Don't run the projects query until user.id exists, and show the loading state until then."

Ohne die Prüfung schickt der erste Aufbau eine Anfrage ohne ID. Mit ihr wartet der erste Aufbau, und die einzige Anfrage ist die, die funktioniert.

Solltest du die Spalte auf text umstellen?

Nein, außer die gespeicherten IDs sind gar keine uuids. Die Spalte umzustellen oder sie in einer Abfrage nach text zu casten, entfernt die Prüfung, die den Fehler ausgelöst hat, und lässt die fehlende ID, wo sie war.

Verfolg dieselbe Anfrage nach der Änderung. user_id=eq.undefined ist jetzt eine gültige Frage, also vergleicht Postgres das Wort undefined mit jeder ID in der Spalte, findet keinen Treffer und gibt eine leere Liste zurück. Deine Seite zeigt "Noch keine Projekte" jemandem, der zehn hat, und in der Konsole steht nichts, was es erklärt. Beim Schreiben geht es weiter: Die Zeile wird mit undefined an der Stelle gespeichert, wo die ID ihres Besitzers hingehört, und eine Abfrage für den echten Besitzer findet sie nie. An der Sortiermaschine heißt das: den Etikettenleser abstecken. Das Paket läuft durch, passt zu keiner Adresse und wird nie zugestellt.

Der eine Fall, in dem die Spalte das Richtige zum Ändern ist, ist die Zeile "user_…" in der Tabelle oben. Die IDs von Clerk sind Text, und Clerks eigene Anleitung zu Supabase legt die Spalte als user_id text not null default auth.jwt()->>'sub' an. Das ist ein anderes Problem: Die Tabelle wurde für die IDs von Supabase gebaut und bekommt die eines anderen Dienstes.

Eine uuid-Spalte lehnt das Wort ab und sagt es. Eine text-Spalte nimmt es an, findet keine Zeile und gibt deiner Seite eine leere Liste ohne Fehler.

Hält die ID in deinem Filter fremde Zeilen draußen?

Nein. Der Filter entscheidet, nach welchen Zeilen deine Seite fragt. Welche Zeilen ein Besucher lesen darf, entscheidet Row Level Security, die Regeln pro Zeile, die Supabase bei jeder Anfrage mit deinem öffentlichen Schlüssel prüft, und jeder kann deine Anfrage mit einer anderen ID oder ganz ohne erneut schicken.

Eine Methode von supabase-js zeigt den Unterschied. .match() nimmt mehrere Spalten auf einmal, und ihr Quellcode lässt jede Spalte weg, deren Wert undefined ist, bevor die Anfrage gebaut wird. Dafür war der Kommentar über eq.undefined da. Ein .match({ user_id: user?.id }), bevor der Nutzer geladen ist, schickt also gar keinen Filter, löst keinen Fehler aus und liefert jede Zeile, die die Regeln der Tabelle diesem Besucher zu lesen erlauben. Häng .single() an diesen Aufruf, und er scheitert anders, mit JSON object requested, multiple (or no) rows returned, weil mehr als eine Zeile zurückkam. Mit eingeschalteten Regeln sind diese Zeilen immer noch nur die des Besuchers. Mit ausgeschalteten sind es die ganze Tabelle, und die Regeln für jede Tabelle einschalten beschreibt den Schalter und die erste Regel.

Ob eine Tabelle einem Fremden Zeilen gibt, siehst du von außen. Unser kostenloser Scan liest deine Live-App mit demselben öffentlichen Schlüssel, den ein Besucher bekommt, meldet, welche Tabellen mit Zeilen antworten, und braucht kein Konto.

Bevor du eine Spalte änderst, die deine App liest

Behalte eine Kopie der Datenbank von vor der Änderung eines Spaltentyps, und erfahre es, wenn eine Tabelle anfängt, Fremden zu antworten.

Reeve Care bewahrt eine Kopie deiner Supabase-Datenbank auf:

  • eine verschlüsselte Kopie nach Zeitplan, auf Reeves eigenem Speicher in der EU, außerhalb deines Supabase-Kontos
  • jede Tabelle mit ihren Zeilen, wie sie waren, bevor du den Typ einer Spalte geändert hast
  • jede Kopie wird geprüft, bevor sie zählt, indem die Zeilen in jeder Tabelle gezählt werden
  • auch deine hochgeladenen Dateien, sobald du eine Storage-Zugangsberechtigung verbindest
  • Wiederherstellung mit einem Klick, vorher wird eine Sicherheitskopie des aktuellen Stands angelegt
  • alles, was Monitor macht

Reeve Monitor prüft weiter von außen:

  • ob deine App erreichbar ist, jede Minute geprüft
  • der vollständige Sicherheitscheck jede Stunde, für bis zu drei Apps
  • eine Nachricht, wenn sich ein Ergebnis ändert, sodass eine Tabelle, die nach einem Deploy Fremden antwortet, dich erreicht, ohne dass du nachsehen musst
  • ein monatlicher Bericht in klarer Sprache über das, was er gesehen hat

Was du jetzt tun solltest

Was zu tun ist

  • Lies den Wert in den Anführungszeichen. Er ist genau das, was deine App geschickt hat.
  • Bei "undefined" oder "" halte die Abfrage zurück, bis es die ID gibt: if (!user?.id) return oder enabled: !!user?.id in TanStack Query.
  • Bei "null" findest du leere Werte mit .is('column', null).
  • Bei "[object Object]" übergib die id des Objekts.
  • Lass die Spalte eine uuid und stell sie nur auf text um, wenn die IDs von einem Dienst kommen, der keine uuids vergibt, etwa Clerk.
  • Liefert ein .match()-Aufruf mit fehlendem Wert Zeilen anderer Leute, schalte Row Level Security für diese Tabelle ein.

Fang mit den Anführungszeichen an, denn der Wert darin zeigt auf die Stelle deiner App, die ihn geschickt hat. Ändert die Lösung, die man dir vorschlägt, den Typ einer Spalte, behalte eine Kopie der Datenbank von vor der Änderung.

FAQ

Was bedeutet invalid input syntax for type uuid?

Postgres sollte einen Wert als uuid lesen, und der Wert ist nicht wie eine uuid geschrieben. Also hat es die Anfrage abgelehnt, bevor irgendetwas gelesen oder geändert wurde. Der Code ist 22P02, eine Supabase-App bekommt ihn als HTTP 400, und der Text in Anführungszeichen am Ende der Meldung ist genau das, was deine App geschickt hat.

Warum ist meine ID undefined?

Weil die Abfrage lief, bevor es den Wert gab, oder weil sie einen Namen liest, unter dem nichts steht. Beim ersten Aufbau einer Seite ist der angemeldete Nutzer womöglich noch nicht gelesen, also ist user?.id undefined, und eine Route, die den Wert projectId nennt, liefert einer Seite, die id liest, jedes Mal undefined. supabase-js schreibt den fehlenden Wert als das Wort undefined in die Anfrage.

Soll ich die Spalte auf text umstellen oder den Wert casten?

Nicht, um diesen Fehler zu beheben. Eine text-Spalte nimmt das Wort undefined an, vergleicht es mit jeder ID, findet keine, und deine Seite zeigt eine leere Liste ganz ohne Fehler. Stell die Spalte nur um, wenn die gespeicherten IDs gar keine uuids sind, etwa Nutzer-IDs von Clerk, die Clerks eigene Supabase-Anleitung in einer text-Spalte ablegt.

Warum passiert das nur beim ersten Laden?

Weil beim nächsten Aufbau die ID da ist und dieselbe Abfrage funktioniert. Die Seite füllt sich vielleicht einen Moment später und sieht gut aus, während jeder Besuch trotzdem eine fehlgeschlagene Anfrage mit undefined darin schickt. Halte die Abfrage zurück, bis die ID existiert, und diese Anfrage wird nie geschickt.

Wie verhindere ich, dass die Abfrage zu früh läuft?

Steig vor dem Aufruf aus, solange die ID fehlt, zum Beispiel mit if (!user?.id) return oben im Effect, und trag user?.id als Abhängigkeit des Effects ein, damit er wieder läuft, sobald die ID auftaucht. Mit TanStack Query setzt du enabled: !!user?.id. Zeig in der Zwischenzeit deinen Ladezustand.

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.