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.

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.
| Feld | Beispiel | Was es dir sagt |
|---|---|---|
code | 22P02 | Postgres' Code für diesen Fehler, in seiner Liste der Codes invalid_text_representation genannt |
message | invalid input syntax for type uuid: "undefined" | Den Typ, den Postgres erwartet hat, und den genauen Wert, den es bekommen hat |
details, hint | null | Mehr sagt Postgres zu diesem Fehler nicht |
| HTTP-Status | 400 Bad Request | So 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ührungszeichen | Was deine App geschickt hat | Wo du suchen solltest |
|---|---|---|
"undefined" | eine Variable, die noch keinen Wert hatte | eine Abfrage, die lief, bevor der Nutzer oder die ID der Seite geladen war; die nächsten zwei Abschnitte |
"" | einen leeren String | ein 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örte | user 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 Supabase | Die IDs von Clerk beginnen mit user_ und sind keine uuids. Der Abschnitt übers Umstellen der Spalte behandelt diesen Fall |
| eine Zahl oder ein Name | einen Teil der Seitenadresse | eine 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.
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 einuserim Zustand also noch leer unduser?.idistundefined. Eine Abfrage, die bei diesem ersten Aufbau startet, schickt genau das. - Die ID hat einen anderen Namen. Eine Route wie
/projects/:projectIdlegt den Wert unterprojectIdab, und eine Seite, dieidliest, bekommt bei jedem Besuchundefined. - 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."
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.
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) returnoderenabled: !!user?.idin TanStack Query. - Bei
"null"findest du leere Werte mit.is('column', null). - Bei
"[object Object]"übergib dieiddes Objekts. - Lass die Spalte eine
uuidund stell sie nur auftextum, 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.