Basi della sicurezza
Supabase "invalid input syntax for type uuid": leggi le virgolette
Supabase "invalid input syntax for type uuid": la tua app ha inviato qualcosa che non è un id. Leggi le virgolette, ferma la query, tieni la colonna.

In breve
- Supabase "invalid input syntax for type uuid" (codice Postgres 22P02, HTTP 400 nella tua app) vuol dire che una colonna uuid ha ricevuto un valore che non è un uuid. Il testo tra virgolette è esattamente quello che ha inviato la tua app.
- "undefined" tra le virgolette vuol dire che la query è partita prima che l'id esistesse, perché supabase-js scrive un valore mancante come quella parola dentro la richiesta. Ferma la query finché l'id non c'è.
- Tieni la colonna uuid. Passarla a text trasforma un errore rumoroso in una pagina che, in silenzio, non mostra niente.
Apri la dashboard della tua app e la lista dei progetti resta vuota per un attimo, poi si riempie. Oppure resta vuota. In entrambi i casi la console del browser ha una riga rossa, e la richiesta che c'è dietro ha risposto così:
{
"code": "22P02",
"details": null,
"hint": null,
"message": "invalid input syntax for type uuid: \"undefined\""
}
Supabase "invalid input syntax for type uuid" vuol dire che Postgres ha ricevuto
un valore che non è un uuid, per una colonna che contiene solo uuid, e ha
rifiutato l'intera richiesta. La parte tra virgolette è quello che ha inviato la
tua app. Qui è la parola undefined: la query è partita prima che esistesse l'id
di cui aveva bisogno.
Le soluzioni rapide che girano per questo errore sono passare la colonna a text
o convertire il valore. Entrambe fanno sparire il messaggio. Nessuna delle due dà
alla query il suo id, quindi la pagina continua a chiedere la cosa sbagliata e
smette di dirtelo.
Che cosa significa "invalid input syntax for type uuid"?
Significa che un valore diretto a una colonna uuid non era scritto come si
scrive un uuid, quindi Postgres si è fermato prima di eseguire qualsiasi cosa.
Nessuna riga è stata letta e nessuna è stata cambiata.
Un uuid è un id di forma fissa: 32 caratteri da 0 a 9 e da a a f, scritti
in gruppi di 8, 4, 4, 4 e 12, come 3f2b8c1e-9a4d-4e7b-b1c2-5d6e7f8a9b0c.
Supabase dà un id così a ogni utente collegato, e una colonna che registra di chi
è ogni riga, come user_id, contiene lo stesso tipo di valore. Il
lettore di Postgres per quel tipo
accetta "a series of 32 hexadecimal digits with an optional dash after each group
of 4 hexadecimal digits, and optionally surrounded by {}". Tutto il resto finisce
con questo messaggio, con l'input citato.
Immagina la colonna come una macchina smistatrice che legge un solo tipo di etichetta: un codice in quei cinque gruppi. La tua app ha messo sul nastro un pacco con una parola scritta dove va il codice. La macchina ha fermato il nastro e ti ha letto la parola.
| Campo | Esempio | Che cosa ti dice |
|---|---|---|
code | 22P02 | Il codice di Postgres per questo errore, chiamato invalid_text_representation nella sua lista di codici |
message | invalid input syntax for type uuid: "undefined" | Il tipo che Postgres si aspettava e il valore esatto che ha ricevuto |
details, hint | null | Postgres non aggiunge altro per questo errore |
| Stato HTTP | 400 Bad Request | Come risponde PostgREST, la parte di Supabase che trasforma le chiamate supabase.from(...) in SQL, a qualsiasi codice senza uno stato proprio (il suo codice) |
Leggi quello che c'è tra le virgolette
Il valore citato è la metà utile del messaggio. È esattamente quello che è arrivato al database, e ogni forma che prende indica una riga diversa della tua app.
| Tra le virgolette | Che cosa ha inviato la tua app | Dove guardare |
|---|---|---|
"undefined" | una variabile che non aveva ancora un valore | una query partita prima che fossero caricati l'utente o l'id della pagina; le due sezioni successive |
"" | una stringa vuota | un valore che parte come '' nello stato della tua app, o un campo del modulo lasciato vuoto |
"null" | null, passato a .eq() | Anche null viene scritto da .eq() come parola. Per trovare le righe in cui la colonna è vuota, la documentazione di supabase-js dice "use .is() instead" |
"[object Object]" | un oggetto intero al posto del suo id | user passato al posto di user.id. JavaScript scrive quel testo per qualsiasi oggetto trasformato in stringa |
"user_…" | un id di un servizio di accesso diverso da quello di Supabase | Gli id di Clerk iniziano con user_ e non sono uuid. La sezione sul cambio di colonna tratta questo caso |
| un numero o un nome | una parte dell'indirizzo della pagina | un URL come /projects/42 o /projects/my-first-app, letto come id |
Se l'errore compare a ogni visita, cerca un nome sotto cui non c'è niente. Se compare solo alla prima visualizzazione della pagina, cerca un valore che non era ancora caricato.
Perché supabase-js invia la parola undefined?
Perché scrive ogni filtro come testo nell'indirizzo della richiesta, e JavaScript
scrive un valore mancante come la parola undefined.
Una chiamata come .eq('user_id', user?.id) diventa ?user_id=eq. seguito dal
valore. Il
codice sorgente della libreria
lo costruisce da un modello, `eq.${value}`, e un commento più in basso nello
stesso file dice che cosa fa questo a un valore mancante: "show up as
?column=eq.undefined". PostgREST legge tutto ciò che segue eq. come testo
semplice, e
il suo parser
tratta null come parola chiave solo dopo is.. Postgres riceve le nove lettere
di undefined e prova a leggerle come un uuid.
.rpc() fa il contrario. I suoi argomenti viaggiano come JSON, che lascia fuori
del tutto un valore undefined, quindi una chiamata di funzione perde l'argomento
invece di inviare la parola, e Supabase risponde
Could not find the function.
In un'app React, cerca il valore mancante in tre punti:
- L'utente non è ancora caricato. Supabase legge la sessione collegata in
modo asincrono (
await supabase.auth.getSession()), quindi alla prima visualizzazione della tua pagina un oggettousertenuto nello stato è ancora vuoto euser?.idèundefined. Una query lanciata in quella prima visualizzazione lo invia. - L'id ha un altro nome. Una route scritta come
/projects/:projectIdmette il valore sottoprojectId, e una pagina che leggeidriceveundefineda ogni visita. - L'id viene da dati ancora in arrivo. Un id preso dal risultato di un'altra query, letto prima che quella query abbia risposto.
Ferma la query finché l'id non esiste
Controlla l'id prima della chiamata e lascia che la pagina mostri il suo stato di
caricamento finché non arriva. Per "undefined" e "" è tutta la soluzione.
useEffect(() => {
if (!user?.id) return
supabase
.from('projects')
.select('*')
.eq('user_id', user.id)
.then(({ data, error }) => {
if (!error) setProjects(data)
})
}, [user?.id])
Il return non invia niente finché manca l'id, e user?.id nella lista in fondo
fa ripartire l'effect appena l'id compare. Sulla macchina smistatrice, è aspettare
che il codice sia sull'etichetta prima di mettere il pacco sul nastro.
Se la tua app carica i dati con TanStack Query (l'hook useQuery), la sua opzione
enabled è lo stesso fermo.
La sua guida alle query dipendenti
imposta enabled: !!userId con la nota "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,
})
Se costruisci con i prompt, chiedilo in una frase. Lasciala in inglese, la lingua in cui ragionano questi strumenti: "Don't run the projects query until user.id exists, and show the loading state until then."
Conviene passare la colonna a text?
No, a meno che gli id che salvi non siano affatto uuid. Cambiare la colonna, o convertirla a text in una query, toglie il controllo che ha sollevato l'errore e lascia l'id mancante dov'era.
Segui la stessa richiesta dopo il cambio. user_id=eq.undefined ora è una
domanda valida, quindi Postgres confronta la parola undefined con ogni id della
colonna, non trova corrispondenze e restituisce una lista vuota. La tua pagina
mostra "Ancora nessun progetto" a qualcuno che ne ha dieci, e nella console non
c'è niente che lo spieghi. In scrittura va oltre: la riga viene salvata con
undefined dove va l'id del suo proprietario, e una query per il proprietario
vero non la trova mai. Sulla macchina smistatrice, è staccare il lettore di
etichette. Il pacco passa, non corrisponde a nessun indirizzo e non viene mai
consegnato.
L'unico caso in cui la cosa da cambiare è la colonna è la riga "user_…" della
tabella sopra. Gli id di Clerk sono testo, e
la guida di Clerk stessa per Supabase
crea la colonna come user_id text not null default auth.jwt()->>'sub'. È un
altro problema: la tabella è stata fatta per gli id di Supabase e riceve quelli di
un altro servizio.
È l'id nel tuo filtro a tenere fuori le righe degli altri?
No. Il filtro decide quali righe chiede la tua pagina. Quali righe un visitatore può leggere lo decide Row Level Security, le regole per riga che Supabase controlla a ogni richiesta fatta con la tua chiave pubblica, e chiunque può rimandare la tua richiesta con un altro id o senza nessuno.
Un metodo di supabase-js mostra la differenza. .match() prende più colonne
insieme, e
il suo codice sorgente
toglie ogni colonna il cui valore è undefined prima di costruire la richiesta. A
questo serviva quel commento su eq.undefined. Quindi un
.match({ user_id: user?.id }) fatto prima che l'utente sia caricato non invia
nessun filtro, non solleva nessun errore e restituisce tutte le righe che le
regole della tabella lasciano leggere a quel visitatore. Aggiungi .single() a
quella chiamata e fallisce in un altro modo, con
JSON object requested, multiple (or no) rows returned,
perché è tornata più di una riga. Con le regole attive, quelle righe restano solo
quelle del visitatore. Con le regole spente, sono l'intera tabella, e
attivarle su ogni tabella spiega
l'interruttore e la prima regola.
Se una tabella consegna righe a uno sconosciuto si vede da fuori. La nostra scansione gratuita legge la tua app online con la stessa chiave pubblica che riceve un visitatore, segnala quali tabelle rispondono con righe e non richiede un account.
Prima di cambiare una colonna che la tua app legge
Tieni una copia del database di prima di cambiare il tipo di una colonna, e vieni avvisato quando una tabella comincia a rispondere agli sconosciuti.
Reeve Care conserva una copia del tuo database Supabase:
- una copia cifrata secondo un calendario, sullo spazio di archiviazione di Reeve nell'UE, fuori dal tuo account Supabase
- ogni tabella con le sue righe com'erano prima che cambiassi il tipo di una colonna
- ogni copia verificata prima di contare, contando le righe di ogni tabella
- anche i tuoi file caricati, appena colleghi una credenziale di Storage
- un ripristino con un clic, con una copia di sicurezza dello stato attuale presa prima
- tutto quello che fa Monitor
Reeve Monitor continua a controllare da fuori:
- se la tua app è raggiungibile, controllato ogni minuto
- il controllo di sicurezza completo ogni ora, su un massimo di tre app
- un messaggio quando un risultato cambia, così una tabella che dopo un deploy comincia a rispondere agli sconosciuti ti raggiunge senza che tu debba andare a guardare
- un report mensile, in linguaggio semplice, di quello che ha visto
Che cosa fare adesso
Cosa fare
- Leggi il valore tra le virgolette. È esattamente quello che ha inviato la tua app.
- Con
"undefined"o"", ferma la query finché l'id non esiste:if (!user?.id) return, oppureenabled: !!user?.idin TanStack Query. - Con
"null", trova i valori vuoti con.is('column', null). - Con
"[object Object]", passa l'iddell'oggetto. - Tieni la colonna
uuide passala atextsolo quando gli id vengono da un servizio che non emette uuid, come Clerk. - Se una chiamata
.match()con un valore mancante restituisce righe di altre persone, attiva Row Level Security su quella tabella.
Comincia dalle virgolette, perché il valore che contengono indica la riga della tua app che lo ha inviato. Se la soluzione che ti propongono cambia il tipo di una colonna, tieni una copia del database di prima del cambio.
FAQ
Che cosa significa invalid input syntax for type uuid?
Significa che Postgres doveva leggere un valore come uuid e il valore non è scritto come un uuid, quindi ha rifiutato la richiesta prima di leggere o cambiare qualsiasi cosa. Il codice è 22P02, un'app Supabase lo riceve come HTTP 400, e il testo tra virgolette alla fine del messaggio è esattamente quello che ha inviato la tua app.
Perché il mio id è undefined?
Perché la query è partita prima che il valore esistesse, oppure perché legge un nome sotto cui non c'è niente. Alla prima visualizzazione di una pagina l'utente collegato potrebbe non essere ancora stato letto, quindi user?.id è undefined, e una route che chiama il valore projectId dà undefined ogni volta a una pagina che legge id. supabase-js scrive il valore mancante nella richiesta come la parola undefined.
Devo passare la colonna a text o convertire il valore?
Non per risolvere questo. Una colonna text accetta la parola undefined, la confronta con ogni id, non ne trova nessuno, e la tua pagina mostra una lista vuota senza alcun errore. Cambia la colonna solo quando gli id che salvi non sono uuid, come gli id utente di Clerk, che la guida di Clerk stessa per Supabase tiene in una colonna text.
Perché succede solo al primo caricamento?
Perché alla visualizzazione successiva l'id è arrivato e la stessa query funziona. La pagina può riempirsi un attimo dopo e sembrare a posto, mentre ogni visita invia comunque una richiesta fallita con dentro undefined. Ferma la query finché l'id non esiste e quella richiesta non parte mai.
Come impedisco alla query di partire troppo presto?
Esci prima della chiamata finché manca l'id, per esempio con if (!user?.id) return in cima all'effect, e metti user?.id tra le dipendenze dell'effect, così riparte appena l'id compare. Con TanStack Query, imposta enabled: !!user?.id. Nel frattempo mostra il tuo stato di caricamento.