fix(postgrest): return a structured error for non-JSON body on successful responses#2398
Conversation
…sful responses A 2xx response is not a guarantee of a valid JSON body. A proxy, gateway, or CDN in front of PostgREST can return a non-JSON body (e.g. an HTML error page) with a success status, and a dropped connection can yield a truncated body. Previously `JSON.parse` ran unguarded on the success path, so a raw `SyntaxError` would either reject `.throwOnError()` (breaking its documented `PostgrestError` contract) or be swallowed by the network-error handler and reported as a misleading `status: 0` failure even though the request reached the server. Wrap the success-path parse in a try/catch that mirrors the existing non-2xx branch: surface a structured error, preserve the real HTTP status, and throw a `PostgrestError` when `throwOnError()` is set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@supabase/auth-js
@supabase/functions-js
@supabase/postgrest-js
@supabase/realtime-js
@supabase/storage-js
@supabase/supabase-js
commit: |
mandarini
left a comment
There was a problem hiding this comment.
Thanks for the careful analysis, the two failure modes you identified
(SyntaxError escaping .throwOnError() and the misleading status: 0 when
the request actually reached the server) are real, and the tests are
well-targeted. A few things I'd like to see before this merges, all in the
direction of less code:
-
Mirror the existing non-2xx fallback exactly. The non-2xx branch
already handles non-JSON bodies aserror = { message: body }— three
lines, no truncation, no hint, no custom message format. The success path
should do the same so the two non-JSON branches produce the same error
shape. Something like:try { data = JSON.parse(body) } catch { error = { message: body } data = null if (this.shouldThrowOnError) { throw new PostgrestError({ message: body, details: '', hint: '', code: '' }) } }
That fixes both bugs (the
SyntaxErrorleak, andstatus: 0— which is
fixed implicitly because we no longer fall into the network-error handler)
without introducing a third error shape alongside the existing PostgREST
structured errors and the non-2xx fallback. -
Drop the 500-byte snippet truncation, the
hintparagraph, and the
"Failed to parse … : <parseError.message>"message format. These are
speculative UX polish, not part of the bug fix:- The hint guesses at the user's infrastructure ("proxy, gateway, or
CDN…") — it could be wrong and is worse than no hint when it is. - The 500-byte cap is arbitrary and needs its own test (the one you
wrote) just to defend that arbitrary choice. - The custom message format becomes a new string consumers may match
against.
If we later want a richer error shape, that's a separate refactor that
should bring both the success and non-2xx fallbacks along together. - The hint guesses at the user's infrastructure ("proxy, gateway, or
-
Drop the long inline comment. The rationale already lives in the PR
description and the commit message — that's where readers go for "why
this exists." A one-line comment is fine if it adds something the code
doesn't already say.
With those changes the tests will simplify too, the truncation test goes
away, and the others stay roughly the same since the structured-error
assertions become error.message === <body> checks.
… fallback Address review feedback on supabase#2398. The success-path JSON.parse guard now mirrors the existing non-2xx fallback exactly (error = { message: body }) instead of introducing a separate structured-error shape. This still fixes both bugs the PR targets — the SyntaxError escaping .throwOnError() and the misleading status: 0 — since the fix is the try/catch itself, not the error shape. Drops the arbitrary 500-byte body truncation, the speculative hint, the custom message format, and the long inline comment (and the test that only existed to defend the truncation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
as you asked |
|
Thank you! |
This PR updates @supabase/*-js libraries to version 2.107.0. **Source**: manual **Changes**: - Updated @supabase/supabase-js to 2.107.0 - Updated @supabase/auth-js to 2.107.0 - Updated @supabase/realtime-js to 2.107.0 - Updated @supabase/postgest-js to 2.107.0 - Refreshed pnpm-lock.yaml --- ## Release Notes ## v2.107.0 ## 2.107.0 (2026-06-02) ### 🚀 Features - **auth:** remove navigator.locks-based mutex; introduce commit guard + dispose() ([#2392](supabase/supabase-js#2392)) - **realtime:** allow httpSend to send binary payload ([#2400](supabase/supabase-js#2400)) - **supabase:** update X-Client-Info to structured metadata format ([#2359](supabase/supabase-js#2359)) ### 🩹 Fixes - **auth:** return AuthInvalidJwtError from getClaims for expired JWT ([#2395](supabase/supabase-js#2395)) - **auth:** recognize ?error= redirects in implicit grant gate ([#2407](supabase/supabase-js#2407)) - **auth): revert fix(auth:** encode client-id in oauth requests ([#2383](supabase/supabase-js#2383), [#2417](supabase/supabase-js#2417)) - **postgrest:** return a structured error for non-JSON body on successful responses ([#2398](supabase/supabase-js#2398)) - **release:** pin workspace:* sibling deps before JSR publish ([#2418](supabase/supabase-js#2418)) - **release:** publish gotrue-js legacy mirror via pnpm ([#2419](supabase/supabase-js#2419)) ### ❤️ Thank You - Claude Opus 4.7 (1M context) - Claude Sonnet 4.6 - Eduardo Gurgel - Guilherme Souza - Katerina Skroumpelou @mandarini - Omar Al Matar @Bewinxed - youcef zr @youcefzemmar - youcefzemmar This PR was created automatically. Co-authored-by: supabase-workflow-trigger[bot] <266661614+supabase-workflow-trigger[bot]@users.noreply.github.com>
Description
On the success path (
res.ok === true),postgrest-jscallsJSON.parse(body)with no error handling. A 2xx status does not guarantee a JSON body:upstream connect error— with a success status.Unexpected end of JSON input) even from a perfectly well-behaved server.When that happens today the raw
SyntaxErrorescapesprocessResponse, and the result depends on how the query was awaited:.throwOnError()→ the promise rejects with aSyntaxError, not aPostgrestError. This breaks the documented contract —catchblocks that readerror.code/error.hint/error.detailsreceive an unexpected shape.{ status: 0, error: { message: "SyntaxError: Unexpected token ..." } }.status: 0signals a client-side network failure, which is misleading: the request actually reached the server and returned a real HTTP status.The non-2xx branch already tolerates non-JSON bodies (it falls back to
error = { message: body }). This PR makes the success path symmetric with it.Change
Wrap the success-path
JSON.parsein atry/catch. On parse failure:message,detailswith a truncated body snippet, an actionablehint, emptycode— matching the existing client-side error shape),status/statusTextinstead of reportingstatus: 0,PostgrestErrorwhen.throwOnError()is set.Valid-JSON responses are completely unaffected — this is purely defensive.
Type of Change
Testing
packages/core/postgrest-js/test/fetch-errors.test.ts, mocked fetch — no Docker): HTML body on 200, truncated JSON, real-status preservation, oversized-body truncation indetails, and thethrowOnError → PostgrestErrorcontract.nx type-check/nx type-check:testpass.nx format:checkpasses; mock-based suites (fetch-errors,retry,timeout-and-url-length) pass.Checklist
nx format)fix(postgrest): ...)Related context:
supabase/postgrest-js#646andsupabase/postgrest-js#282(the "Unexpected token < in JSON" class of reports). This PR intentionally scopes to client-side robustness/contract-correctness rather than expecting upstream proxies to always emit JSON.🤖 Generated with Claude Code