Skip to content

fix(postgrest): honour throwOnError when maybeSingle finds multiple rows - #2580

Merged
mandarini merged 1 commit into
supabase:masterfrom
Zuhef:fix/alpha-2579-maybesingle-throwonerror
Jul 31, 2026
Merged

fix(postgrest): honour throwOnError when maybeSingle finds multiple rows#2580
mandarini merged 1 commit into
supabase:masterfrom
Zuhef:fix/alpha-2579-maybesingle-throwonerror

Conversation

@Zuhef

@Zuhef Zuhef commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Bug fix

What is the current behavior?

Fixes #2579

Since the fix for postgrest-js#361, .maybeSingle() no longer sends Accept: application/vnd.pgrst.object+json — it fetches a list and enforces cardinality client-side, synthesising PGRST116 when more than one row comes back:

if (this.isMaybeSingle && Array.isArray(data)) {
  if (data.length > 1) {
    error = { code: 'PGRST116', /* … */ }
    data = null
    count = null
    status = 406
    statusText = 'Not Acceptable'
  } else if ()

That branch never consults this.shouldThrowOnError. Every other error path in processResponse does — the non-2xx path ends with if (error && this.shouldThrowOnError) throw new PostgrestError(error) — and then() has no post-hoc throw, so throwing only ever happens at those sites.

The result is that .maybeSingle().throwOnError() resolves with an error object instead of rejecting, which contradicts what throwOnError() documents ("reject the promise by throwing the error instead of returning it as part of a successful response").

That fails in the unhelpful direction. Code written as

const { data } = await supabase.from('users').select().maybeSingle().throwOnError()

destructures only data precisely because it trusts throwOnError() to raise. With duplicate rows it quietly gets data === null and no exception, so the condition is swallowed rather than surfaced.

What is the new behavior?

The synthesised PGRST116 now rejects with a PostgrestError when throwOnError() is active, matching what happens when PostgREST itself returns that error.

Behaviour without throwOnError() is unchanged: still status: 406, statusText: 'Not Acceptable', error.code: 'PGRST116', data: null. The returned error object is untouched — including hint: null, which existing snapshots in basic.test.ts and transforms.test.ts assert. PostgrestError types hint as string, so the throw passes hint: error.hint ?? '', consistent with the other synthesised throw in this file which uses hint: ''.

Additional context

maybeSingle() + throwOnError() had no test coverage — the existing PGRST116 assertions check the returned error object rather than the throwing path, and they need the Dockerised PostgREST. The new test/maybe-single.test.ts uses a mocked fetch, so it runs offline and pins all four cardinality outcomes: one row, zero rows, multiple rows, and multiple rows with throwOnError().

Verification, from packages/core/postgrest-js:

$ npx jest --runInBand test/maybe-single.test.ts
Tests:  4 passed, 4 total

Before the source change, the new suite is 1 failed, 3 passed, with:

● maybeSingle cardinality handling › honours throwOnError() when multiple rows are returned
  expect(received).rejects.toThrow()
  Received promise resolved instead of rejected

Across the suites that run without Docker (fetch-errors, headers-serialization, retry, max-affected, permission-hints, maybe-single): 45 passed / 6 failed with this change, versus 44 passed / 7 failed on an unmodified checkout — the one flipped test is the new one, and nothing else moved. The 6 remaining failures are all in max-affected.test.ts and fail with ECONNREFUSED; they need the local PostgREST from docker compose, which isn't available in my environment, so those are untouched-but-unverified by me.

Also checked:

  • tsc --noEmit --project tsconfig.json — clean.
  • tsc --noEmit --project tsconfig.test.json — clean.
  • prettier --check — clean on both files.

The client-side cardinality check added for postgrest-js#361 synthesises the PGRST116 error itself, but that branch never consulted shouldThrowOnError. Every other error path in processResponse does, and then() has no post-hoc throw, so .maybeSingle().throwOnError() resolved with an error object instead of rejecting.

Callers that destructure only data because they trust throwOnError() to raise silently received data === null on duplicate rows. Add the missing check plus offline tests covering all four maybeSingle cardinality outcomes.
@Zuhef
Zuhef requested review from a team as code owners July 29, 2026 21:41
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c80d4fec-30b4-44d2-9384-382895da1e73

📥 Commits

Reviewing files that changed from the base of the PR and between 6331898 and e7a101b.

📒 Files selected for processing (2)
  • packages/core/postgrest-js/src/PostgrestBuilder.ts
  • packages/core/postgrest-js/test/maybe-single.test.ts

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Improved maybeSingle() handling for responses containing multiple records.
    • Multiple-record responses now consistently return a “Not Acceptable” error with no data.
    • Improved error details when .throwOnError() is enabled, ensuring the request rejects with a properly formatted error.
  • Tests

    • Added coverage for single-record, empty, multiple-record, and error-throwing scenarios.

Walkthrough

maybeSingle() now honors .throwOnError() when multiple rows are returned. The synthesized PGRST116 error is converted into a PostgrestError, with a string-valued hint. Tests cover single-row unwrapping, empty results, multi-row 406 responses, and promise rejection when throwing is enabled.

Assessment against linked issues

Objective Addressed Explanation
Ensure maybeSingle().throwOnError() rejects on multiple rows with a PGRST116 PostgrestError [#2579]

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

@supabase/auth-js

npm i https://pkg.pr.new/@supabase/auth-js@2580

@supabase/functions-js

npm i https://pkg.pr.new/@supabase/functions-js@2580

@supabase/postgrest-js

npm i https://pkg.pr.new/@supabase/postgrest-js@2580

@supabase/realtime-js

npm i https://pkg.pr.new/@supabase/realtime-js@2580

@supabase/storage-js

npm i https://pkg.pr.new/@supabase/storage-js@2580

@supabase/supabase-js

npm i https://pkg.pr.new/@supabase/supabase-js@2580

commit: e7a101b

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 93.355% (+12.1%) from 81.305% — Zuhef:fix/alpha-2579-maybesingle-throwonerror into supabase:master

@mandarini mandarini left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

@mandarini
mandarini merged commit 4c13bf0 into supabase:master Jul 31, 2026
30 checks passed
mandarini pushed a commit to supabase/ssr that referenced this pull request Aug 11, 2026
This PR updates `@supabase/supabase-js` to v2.112.3.

**Source**: manual

---

## Release Notes

## v2.112.3

## 2.112.3 (2026-08-11)

### 🩹 Fixes

- **supabase:** add trace context headers to canonical CORS allow-list
([#2603](supabase/supabase-js#2603))
- **supabase:** improve trace propagation sampling and diagnostics
([#2604](supabase/supabase-js#2604))

### ❤️ Thank You

- Katerina Skroumpelou @mandarini
## v2.112.2

## 2.112.2 (2026-08-06)

### 🩹 Fixes

- **realtime:** prevent duplicate on bindings
([#2594](supabase/supabase-js#2594))
- **realtime:** clear stale join payload on sign-out
([#2597](supabase/supabase-js#2597))

### ❤️ Thank You

- Filipe Cabaço @filipecabaco
- Vaibhav @7ttp
## v2.112.1

## 2.112.1 (2026-08-05)

### 🩹 Fixes

- **auth:** preserve 5xx error message
([#2587](supabase/supabase-js#2587))
- **realtime:** ensure setAuth doesn't disable token refresh
([#2592](supabase/supabase-js#2592))

### ❤️ Thank You

- Eduardo Gurgel
- Vaibhav @7ttp
## v2.112.0

## 2.112.0 (2026-08-03)

### 🚀 Features

- **supabase:** move OpenTelemetry tracing to opt-in /tracing subpath
([#2583](supabase/supabase-js#2583))

### 🩹 Fixes

- **auth:** accept uppercase UUIDs in validateUUID
([#2467](supabase/supabase-js#2467))
- **postgrest:** honour throwOnError when maybeSingle finds multiple
rows ([#2580](supabase/supabase-js#2580))
- **storage:** resolve createSignedUrls return type mismatch
([#2474](supabase/supabase-js#2474))
- **storage:** expose service error code on StorageApiError
([#2537](supabase/supabase-js#2537))
- **supabase:** forward db retry option
([#2571](supabase/supabase-js#2571))

### ❤️ Thank You

- Anubhav Anand @i-anubhav-anand
- Gourab Singha @gourabsingha1
- Juhef @juheff
- Katerina Skroumpelou @mandarini
- Thribhuvan
- Vaibhav @7ttp
- Zuhef Ahmed @Zuhef

This PR was created automatically.

Co-authored-by: supabase-workflow-trigger[bot] <266661614+supabase-workflow-trigger[bot]@users.noreply.github.com>
mandarini pushed a commit to supabase/supabase that referenced this pull request Aug 11, 2026
This PR updates @supabase/*-js libraries to version 2.112.3.

**Source**: manual

**Changes**:
- Updated @supabase/supabase-js to 2.112.3
- Updated @supabase/auth-js to 2.112.3
- Updated @supabase/realtime-js to 2.112.3
- Updated @supabase/postgest-js to 2.112.3
- Refreshed pnpm-lock.yaml

---

## Release Notes

## v2.112.3

## 2.112.3 (2026-08-11)

### 🩹 Fixes

- **supabase:** add trace context headers to canonical CORS allow-list
([#2603](supabase/supabase-js#2603))
- **supabase:** improve trace propagation sampling and diagnostics
([#2604](supabase/supabase-js#2604))

### ❤️ Thank You

- Katerina Skroumpelou @mandarini
## v2.112.2

## 2.112.2 (2026-08-06)

### 🩹 Fixes

- **realtime:** prevent duplicate on bindings
([#2594](supabase/supabase-js#2594))
- **realtime:** clear stale join payload on sign-out
([#2597](supabase/supabase-js#2597))

### ❤️ Thank You

- Filipe Cabaço @filipecabaco
- Vaibhav @7ttp
## v2.112.1

## 2.112.1 (2026-08-05)

### 🩹 Fixes

- **auth:** preserve 5xx error message
([#2587](supabase/supabase-js#2587))
- **realtime:** ensure setAuth doesn't disable token refresh
([#2592](supabase/supabase-js#2592))

### ❤️ Thank You

- Eduardo Gurgel
- Vaibhav @7ttp
## v2.112.0

## 2.112.0 (2026-08-03)

### 🚀 Features

- **supabase:** move OpenTelemetry tracing to opt-in /tracing subpath
([#2583](supabase/supabase-js#2583))

### 🩹 Fixes

- **auth:** accept uppercase UUIDs in validateUUID
([#2467](supabase/supabase-js#2467))
- **postgrest:** honour throwOnError when maybeSingle finds multiple
rows ([#2580](supabase/supabase-js#2580))
- **storage:** resolve createSignedUrls return type mismatch
([#2474](supabase/supabase-js#2474))
- **storage:** expose service error code on StorageApiError
([#2537](supabase/supabase-js#2537))
- **supabase:** forward db retry option
([#2571](supabase/supabase-js#2571))

### ❤️ Thank You

- Anubhav Anand @i-anubhav-anand
- Gourab Singha @gourabsingha1
- Juhef @juheff
- Katerina Skroumpelou @mandarini
- Thribhuvan
- Vaibhav @7ttp
- Zuhef Ahmed @Zuhef
## v2.111.0

## 2.111.0 (2026-07-28)

### 🚀 Features

- **auth:** store PKCE verifiers in per-flow slots to survive
overlapping flows
([#2569](supabase/supabase-js#2569))

### ❤️ Thank You

- Katerina Skroumpelou @mandarini

This PR was created automatically.

Co-authored-by: supabase-workflow-trigger[bot] <266661614+supabase-workflow-trigger[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

postgrest-js: maybeSingle() multi-row error bypasses throwOnError()

3 participants