PostgREST · PGRST116

PGRST116: JSON object requested, multiple (or no) rows returned

Luca Urti

JSON object requested, multiple (or no) rows returned

Why does .single() fail with “multiple (or no) rows returned”?

.single() promises the caller exactly one object and asks PostgREST to enforce it, so the request fails when the query matched no rows or more than one. The two halves have opposite causes. Zero rows usually means the row exists but Row Level Security filtered it out for this user — RLS is applied before the count, so a row you can see in the dashboard is genuinely absent from the API's point of view. More than one means your filter is not as unique as you assumed, typically a lookup on a column with no unique constraint.

Zero rows: check RLS before you check your filter

Run the same query in the SQL editor, which bypasses RLS, and then check whether a policy covers this row for this role. If the editor returns the row and the API does not, the policy is the answer and no amount of adjusting the client-side filter will help.

.maybeSingle() is the right call when zero is a legitimate outcome — a profile that may not exist yet, an optional record. It returns null instead of raising. It does not make an RLS problem go away; it makes it silent, which is worse if the row was supposed to be visible.

More than one row: the constraint is missing, not the filter

If a lookup by email or slug returns two rows, the database allows duplicates that your code assumes cannot exist. Adding .limit(1) hides that and picks an arbitrary one, which for a lookup on an identity column means a user can occasionally be served the wrong record. The fix is a unique constraint, and finding out why a duplicate exists.

The fix

the two correct responses

// Zero is legitimate — a profile row that may not have been created yet.
const { data, error } = await supabase
  .from("profiles")
  .select("*")
  .eq("id", user.id)
  .maybeSingle();          // null instead of an error

// Zero is not legitimate — enforce it in the database, not the client.
-- alter table public.profiles add constraint profiles_email_key unique (email);

The fix that works and costs you the database

The response that makes this error vanish everywhere is to drop .single() and widen the query — removing the .eq() that returned nothing, or moving the call to a server route with the service_role key so RLS stops filtering. Both turn a failed lookup for one row into a successful fetch of rows the user was never entitled to, and the second one does it silently across every table.

Whether the trap is already in your repo is a question you can answer

Sentris reads the SQL and the client code, so it reports the shortcut above where it was actually taken — a service_role key in a browser bundle, a policy that is using (true), a table with RLS switched off. A scan needs no account and no card. How often it is wrong is measured and published.

Scan my app