Vai al contenuto

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.

Vlad Tkachenko10 min di lettura
Un'etichetta da pacco con lo spazio del codice diviso in cinque caselle e, davanti, un cartellino con undefined in ambra, troppo lungo.

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.

CampoEsempioChe cosa ti dice
code22P02Il codice di Postgres per questo errore, chiamato invalid_text_representation nella sua lista di codici
messageinvalid input syntax for type uuid: "undefined"Il tipo che Postgres si aspettava e il valore esatto che ha ricevuto
details, hintnullPostgres non aggiunge altro per questo errore
Stato HTTP400 Bad RequestCome 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 virgoletteChe cosa ha inviato la tua appDove guardare
"undefined"una variabile che non aveva ancora un valoreuna query partita prima che fossero caricati l'utente o l'id della pagina; le due sezioni successive
""una stringa vuotaun 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 iduser 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 SupabaseGli id di Clerk iniziano con user_ e non sono uuid. La sezione sul cambio di colonna tratta questo caso
un numero o un nomeuna parte dell'indirizzo della paginaun 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.

supabase-js mette il valore del filtro nella richiesta come testo. Un valore mancante arriva come la parola undefined, e la colonna uuid lo rifiuta.

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 oggetto user tenuto nello stato è ancora vuoto e user?.id è undefined. Una query lanciata in quella prima visualizzazione lo invia.
  • L'id ha un altro nome. Una route scritta come /projects/:projectId mette il valore sotto projectId, e una pagina che legge id riceve undefined a 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."

Senza il controllo, la prima visualizzazione invia una richiesta senza id. Con il controllo, la prima visualizzazione aspetta, e l'unica richiesta inviata è quella che funziona.

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.

Una colonna uuid rifiuta la parola e lo dice. Una colonna text la accetta, non trova nessuna riga e consegna alla tua pagina una lista vuota senza errori.

È 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, oppure enabled: !!user?.id in TanStack Query.
  • Con "null", trova i valori vuoti con .is('column', null).
  • Con "[object Object]", passa l'id dell'oggetto.
  • Tieni la colonna uuid e passala a text solo 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.

Scritto da

Vlad Tkachenko

Fondatore di Reeve

Passo le giornate a guardare app costruite con Lovable, Bolt, v0, Cursor e Replit, e la breve lista di errori che vi ricompaiono di continuo.

Altro sull'autore

Da leggere dopo

Tutti gli articoli

Fai il backup del database Supabase prima della prossima modifica

Reeve ne tiene una copia cifrata a intervalli programmati e la ripristina per te se qualcosa va storto.

Vedi i backup Supabase

Controllo esterno automatizzato, non un audit completo. L'assenza di risultati non è una garanzia di sicurezza.