Basi della sicurezza
Supabase "multiple (or no) rows returned" quasi sempre è nessuna
Supabase restituisce PGRST116, "JSON object requested, multiple (or no) rows returned", quando la query non ha trovato nulla. Una riga nascosta sembra assente.

In breve
- "JSON object requested, multiple (or no) rows returned" è l'errore PGRST116, e significa che la tua query è tornata con un numero di righe diverso da uno. Quasi sempre quel numero è zero.
- Con Row Level Security attiva, una riga che il tuo visitatore non può leggere torna come nessuna riga. Così un record che vedi nel Table Editor di Supabase può mancare nella tua app mentre i dati stanno benissimo.
- Usa maybeSingle() dove non trovare nulla è una risposta legittima e single() dove non lo è. Prima lancia una query nel SQL Editor, per sapere quale dei due casi hai.
La tua app dice a qualcuno che un record non si trova. Apri il Table Editor di Supabase e il record sta lì, nella tabella, con l'id giusto.
Questo è quello che è tornato dal database:
{
"code": "PGRST116",
"message": "JSON object requested, multiple (or no) rows returned",
"details": "Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row"
}
Ed ecco la parte che guida dopo guida sbaglia: zero righe è una risposta che le
tue stesse regole sanno produrre, e si legge esattamente come dei dati spariti.
Cerca questo messaggio e ti diranno di sostituire .single() con
.maybeSingle(). L'errore si ferma davvero. Non ti dice niente sul fatto che la
riga mancasse o che il tuo visitatore non avesse mai il permesso di vederla, e
questi due casi si riparano in modo diverso.
Che cosa significa "JSON object requested, multiple (or no) rows returned"?
La tua query ha chiesto al database esattamente una riga e il risultato ne conteneva un altro numero. Quasi sempre quel numero è zero.
Pensa a una sala di lettura dove compili un modulo e qualcuno dietro il bancone va a prendere il libro. Il tuo modulo dice: portami l'unico libro con questo numero. Il bancone torna e ti dice che non c'è niente che corrisponda al modulo. Quella è l'unica risposta che il bancone dà, ed è la stessa sia che il libro non sia mai stato a catalogo, sia che stia su uno scaffale per cui non hai l'autorizzazione. Nessuno sta giocando con te. Il sistema dei moduli è stato costruito perché ciò per cui non hai l'autorizzazione sia semplicemente assente dalla tua vista del catalogo.
È la tua app a tenere il modulo, e il numero con cui il bancone è tornato è
scritto nella riga details della risposta.
Due messaggi diversi, e lo stesso codice di errore
Il codice è stabile e la frase no, quindi regolati su PGRST116.
Per questo singolo errore circolano due formulazioni, e quale vedi dipende da
quale versione di PostgREST gira il tuo progetto. PostgREST è il server che
trasforma le tue tabelle Supabase nell'API HTTP con cui parla la tua app. La sua
documentazione per la versione 12 dà il messaggio
JSON object requested, multiple (or no) rows returned, con una riga details
che dice
Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row. Nelle
versioni 13 e 14 la stessa situazione è documentata come
Cannot coerce the result to a single JSON object, con details che dice
The result contains 0 rows.
La frase più vecchia ha una seconda vita oltre i progetti vecchi. Quando chiami
.maybeSingle() e corrisponde più di una riga, il client JavaScript di Supabase
scrive l'errore da sé invece di passarne uno dal server, e il messaggio che
scrive è la formulazione vecchia. Quindi la prima frase arriva ancora in app il
cui database avrebbe mandato la seconda.
Quello che non si muove è il codice. La tabella degli errori di PostgREST elenca
PGRST116 sotto 406 in tutte e tre le versioni, con la stessa descrizione: più
di una riga, o nessuna, quando ne era richiesta esattamente una. Leggi il campo
code e ignora la prosa.
Perché .single() produce tutto questo
Perché .single() è un'istruzione sulla forma della risposta e non un filtro sui
dati.
Quando scrivi .single(), il client di Supabase aggiunge un header alla
richiesta: Accept: application/vnd.pgrst.object+json. Quell'header chiede al
database di mandare la riga da sola, come record. È quello che permette al tuo
codice di scrivere data.name dove altrimenti dovrebbe scrivere data[0].name.
Il database lo rispetta alla lettera. Una riga torna come record. Qualsiasi altro
numero di righe non si può mandare in quella forma, quindi la risposta è un 406 e
un oggetto errore. Togli il .single() e la query identica restituisce [] con
un 200 e nessun errore da nessuna parte.
Quindi non è servito che si rompesse niente per produrre questo. Un risultato vuoto è diventato un errore perché la tua query diceva che la risposta doveva contenere esattamente un record.
Causa 1: la riga c'è e le tue regole dicono di no
Row Level Security non rifiuta la tua query. Le toglie delle righe, e una riga tolta non è mai stata nel risultato che viene contato.
Row Level Security è l'insieme di regole su ogni tabella Supabase che decide,
riga per riga, quali righe un dato visitatore può leggere. Quando nessuna regola
consente al tuo visitatore di leggere la riga che la tua query ha nominato,
quella riga viene tolta prima che il risultato sia misurato. Il conteggio è zero.
Il tuo .single() ne voleva una. Ottieni PGRST116, e nessuna parte della
risposta accenna al fatto che ci fosse di mezzo una regola.
È di nuovo la sala di lettura, ed è per questo che il bancone dice due volte la stessa cosa. La tua autorizzazione viene applicata prima ancora che lo scaffale sia cercato, quindi un libro che non puoi prendere e un libro che non esiste producono un modulo identico che torna indietro.
È anche per questo che il Table Editor ti inganna. Il SQL Editor e il Table Editor della tua console Supabase girano con privilegi a cui le tue policy non si applicano, quindi ti mostrano ogni riga. La richiesta della tua app porta la tua publishable key e la sessione del tuo visitatore, e a lei le policy si applicano. Due viste di una stessa tabella, e solo una è quella che ricevono i tuoi utenti.
Questa differenza si legge da fuori, ed è così che possiamo contarla. Il nostro scanner legge le app in rete come farebbe uno sconosciuto. Dietro 8.435 delle app che ha valutato c'è un progetto Supabase. Su 3.680 di quelle la domanda "qualcuno senza account può leggere questa tabella" ha avuto una risposta utilizzabile, e 1.584 hanno risposto di no: uno sconosciuto che chiedeva le righe non ha ricevuto niente. Quel niente è lo stesso niente che riceve la tua app quando nessuna policy le corrisponde.
Come distinguere una riga nascosta da una che manca
Lancia la stessa ricerca due volte, una dove le policy si applicano e una dove non si applicano. Una query chiude la questione.
- Apri la tua console Supabase, vai nel SQL Editor e prendi la riga con lo
stesso valore che ha usato la tua app:
select * from your_table where id = 'the-id-your-app-sent';. Una query dal SQL Editor gira attraverso il ruolopostgres, a cui le tue policy non si applicano. - Se restituisce la riga, la riga esiste e qualcosa tra la tua app e la riga l'ha tolta. Quel qualcosa è una policy, e sei nella causa 1. Leggi le policy di quella tabella.
- Se non restituisce niente nemmeno lì, la riga manca davvero e sei nella causa 3 più sotto. Le tue regole non c'entrano.
Fallo prima di cambiare una riga di codice. Le due cause producono lo stesso oggetto errore, e le due riparazioni non hanno niente in comune.
Già che guardi le policy di quella tabella, vale la pena sapere che cosa dicono oggi a qualcuno senza nessun account. La nostra scansione gratuita legge la tua app in rete e ti dice quali delle tue tabelle rispondono a uno sconosciuto. Dura una ventina di secondi e non richiede un account: scansiona la tua app.
Causa 2: sono tornate più righe
.single() è stato usato su una ricerca che può corrispondere a più di una riga.
Una ricerca per chiave primaria corrisponde a una riga o a nessuna, per definizione. Una ricerca per email, per slug, per nome, per stato, per chiave esterna: ognuna di queste può toccarne due. La storia solita è che la colonna aveva un valore per riga nei tuoi dati di prova e ha smesso di averlo quando sono arrivate persone vere.
La risposta ti dice in quale causa sei. Entrambe le formulazioni della riga
details dicono quante righe conteneva il risultato. Zero ti manda alla causa 1
o alla causa 3. Un numero sopra lo zero indica questa, ed è il conteggio delle
righe che hanno corrisposto.
La riparazione non è .maybeSingle(). Accetta zero righe e continua a dare
errore con due, quindi qui non cambia niente. O restringi la query a una riga con
la colonna che è davvero unica, o accetti che ce ne siano diverse e leggi una
lista.
Causa 3: la riga è sparita davvero
A volte la riga manca, e la query della sezione precedente è ciò che lo conferma.
I motivi ordinari, nell'ordine in cui saltano fuori in un'app fatta in vibe coding. Il valore su cui filtrava la tua query era vuoto, perché l'id non era caricato quando la query è partita, quindi non ha corrisposto niente. La riga è stata cancellata, da te o da un prompt che ha riscritto più del previsto. Oppure la query nomina una tabella ricostruita con un altro nome e le righe vecchie sono andate con la tabella vecchia.
Niente di tutto questo è un problema di sicurezza, e nessuno si risolve cambiando una policy. Se il SQL Editor non trova niente, smetti di leggere di Row Level Security e vai a vedere che cosa passa la tua app.
single o maybeSingle: quale vuole la tua query
.maybeSingle() dove non trovare nulla è una risposta legittima. .single()
dove non lo è.
.single() tratta qualsiasi numero di righe diverso da uno come un errore. È il
comportamento giusto per una ricerca per chiave primaria, dove zero righe vuol
dire che il record di cui parla il tuo codice non esiste e andare avanti sarebbe
peggio che fermarsi.
.maybeSingle() accetta zero righe: ti restituisce data come null, senza
errore e con un 200. Più di una riga resta un errore, con lo stesso codice
PGRST116. Quindi va bene per una riga di profilo che potrebbe non essere ancora
stata creata, o per un record facoltativo per cui la tua pagina disegna uno stato
vuoto.
Una cosa a cui fare attenzione. Sostituire .single() con .maybeSingle() cambia
come si comporta la tua app e non cambia niente di quello che il database ha
mandato. Se una policy stava nascondendo la riga, hai trasformato un errore
visibile in una pagina che si disegna come se il record non esistesse, proprio
per l'unica persona che doveva vederlo. L'errore ti stava dicendo qualcosa. Fai
lo scambio dopo la prova qui sopra, quando sai che zero è la risposta onesta.
Tenere d'occhio le tue regole dopo averle cambiate
Una policy che allarghi per togliere di mezzo questo errore è un cambiamento che da dentro la tua app non vede nessuno. La tua sessione continua a funzionare, l'errore è sparito, e se quella tabella adesso risponde a uno sconosciuto è una domanda che qualcuno deve andare a fare.
Reeve Monitor la fa per te:
- tutti e nove i controlli ogni ora, su un massimo di tre app
- un'email il giorno in cui una nuova scansione valuta la tua app peggio della precedente
- se l'app risponde, ogni 60 secondi
- un rapporto mensile di quello che ha visto
Una regola allentata quanto basta per togliere un errore può lasciare che uno sconosciuto cambi le righe oltre che leggerle. Reeve Care tiene una copia del tuo database Supabase dove nessun cambiamento alle tue regole può arrivare.
- una copia cifrata ogni giorno, tenuta fuori dal tuo account Supabase
- ogni copia verificata prima di contare, contando le righe di ogni tabella
- un ripristino in un clic che salva quello che c'è prima di sostituirlo
- anche i tuoi file caricati, appena colleghi un accesso Storage
- tutto quello che fa Monitor
Che cosa fare oggi
Cosa fare
- Regolati sul campo
code.PGRST116arriva sotto due frasi diverse, e quale ti tocca dipende dalla versione di PostgREST che gira il tuo progetto. - Leggi il conteggio di righe nella riga
details. Zero vuol dire risultato vuoto. Sopra lo zero, la tua ricerca ha toccato più righe e serve una colonna unica. - Su un risultato vuoto, lancia la stessa ricerca nel SQL Editor prima di cambiare codice. Se la riga torna lì e non nella tua app, una policy l'ha tolta.
- Passa a
.maybeSingle()quando sai che zero è la risposta onesta. Su una riga che una policy nasconde, sostituisce un errore con una pagina vuota, senza avvisare. - Dopo aver cambiato una policy per togliere questo, controlla che cosa restituisce adesso quella tabella a qualcuno senza account.
Comincia con una query nel SQL Editor, perché è lei a decidere quale metà di questo articolo ti riguarda. Se viene fuori che la risposta era una policy, attivare Row Level Security su ogni tabella mostra come scrivere una regola che lascia passare la persona giusta, e "new row violates row-level security policy" è il messaggio che ricevi quando una policy rifiuta una scrittura che vedi con i tuoi occhi.
FAQ
Che cosa significa PGRST116?
Significa che la tua query ha chiesto al database esattamente una riga e il risultato ne conteneva un altro numero. PostgREST, la parte di Supabase che risponde alla tua app via HTTP, lo restituisce come HTTP 406 con il codice PGRST116. Il numero sta nella riga details della risposta, e quasi sempre è zero.
Perché la mia query non restituisce righe se vedo la riga in Supabase?
Perché il SQL Editor della tua console gira con privilegi a cui le tue policy Row Level Security non si applicano, e la tua app no. Se nessuna policy consente al tuo visitatore di leggere quella riga, la riga viene tolta dal suo risultato prima che il risultato venga contato, e quello che torna è uno zero. La riga c'è. Non è mai stata nella risposta.
Qual è la differenza tra single e maybeSingle?
single() tratta qualsiasi numero di righe diverso da uno come un errore. maybeSingle() accetta zero righe e ti restituisce data come null, senza errore, e continua a dare errore quando più di una riga corrisponde. Quindi maybeSingle() va bene dove non trovare nulla è una risposta legittima, come un profilo che ancora non esiste, e single() va bene per una ricerca su una colonna che può corrispondere a una sola riga.
È Row Level Security a causare questo errore?
È la causa più comune in un'app costruita con Lovable, Bolt, Replit o v0. Row Level Security filtra le righe fuori dal risultato invece di rifiutare la richiesta, quindi una riga che nessuno può leggere è indistinguibile da una riga che non esiste. Entrambe danno un conteggio di zero, e una query che pretende esattamente una riga fallisce su entrambe.
Perché è un 406?
406 Not Acceptable è lo stato di una richiesta a cui il server non può rispondere nella forma richiesta. La tua query portava un header Accept che diceva che la risposta doveva essere un record singolo e non una lista, e senza righe da metterci dentro non c'è nessuna risposta accettabile da mandare. Togli quell'header e la stessa query restituisce una lista vuota con un 200.