Skip to content

refactor: share the HTTP request pieces between the fetch layers - #1647

Open
spydon wants to merge 4 commits into
breaking/shared-exponential-backofffrom
refactor/shared-http-fetch-pieces
Open

refactor: share the HTTP request pieces between the fetch layers#1647
spydon wants to merge 4 commits into
breaking/shared-exponential-backofffrom
refactor/shared-http-fetch-pieces

Conversation

@spydon

@spydon spydon commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Tier 3 of #1572, last of four PRs. Stacked on #1646 (→ #1645#1644).

The tier 3 note said the fetch wrappers are shareable in pieces, not as one wrapper, since each is bound to its own exception type. These are the pieces, now in supabase_common:

Helper Replaces
sendRequest(request, {httpClient}) httpClient != null ? httpClient.send(request) : request.send() in functions, storage (3 sites) and the Iceberg catalog
headerValue(headers, name) case-insensitive header lookups
setDefaultContentType(headers, value) storage's and functions' own "set Content-Type unless the caller already did" checks
responseMediaType(headers) functions' (headers['Content-Type'] ?? headers['content-type'] ?? 'text/plain').split(';')[0].trim().toLowerCase() and the Iceberg catalog's contentType.contains('application/json')
tryDecodeJsonObject(body) the "decode the error body, fall back to the raw body" dance in storage and postgrest

supabase_common gains a dependency on http (^1.6.0, the constraint every other package already uses).

Two bugs this surfaces

Each has a regression test that fails without the fix.

Postgrest swallowed typed errors for maybeSingle(). The decode and the maybeSingle handling shared one try/catch (_):

try {
  final errorJson = jsonDecode(response.body) as Map<String, dynamic>;
  error = PostgrestException.fromJson(...);
  if (_maybeSingle) {
    return _handleMaybeSingleError(response, error); // rethrows `error` for a real failure
  }
} catch (_) {
  error = PostgrestException(message: response.body, ...); // ...and catches it here
}

_handleMaybeSingleError returns null data for the "0 rows" case and rethrows otherwise, but that rethrow landed in the catch (_) and was replaced by a generic exception built from the raw body. So a maybeSingle() request that failed for a real reason lost its errorCode, details and hint. Splitting the decode from the handling fixes it.

A JSON error body with unexpected field types crashed. Removing that catch-all exposed the next layer of the same problem: PostgrestException.fromJson cast code and hint to String and required a String message, so {"code": 502, "message": "Bad gateway"} — a shape gateways really do return — threw a TypeError out of the builder. Both fromJson factories now read every field defensively, so any JSON object yields an exception. StorageException.fromJson had the same casts and the same exposure.

The matching storage bug, where a JSON error body that is not an object escaped as a TypeError, is fixed in #1644; here its inline guard collapses into tryDecodeJsonObject.

Not converted

Postgrest's own send site keeps managing its Client explicitly. It has to close a client it created even when the response body fails midway, and BaseRequest.send only closes its internal client when the stream completes normally.

gotrue and postgrest also keep setting Content-Type unconditionally rather than adopting setDefaultContentType: both always JSON-encode the body, so honouring a caller-provided content type there would mislabel the request rather than respect it.

Testing

New http_test.dart in supabase_common covers each helper, plus the three regression tests described above. melos analyze, dcm analyze and melos format clean; gotrue, postgrest, storage_client, functions_client, realtime_client, supabase, supabase_common and supabase_flutter suites pass. Capability matrix symbol and drift checks pass.

@spydon
spydon requested a review from a team as a code owner August 5, 2026 09:58
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dc416e9f-1c04-4c59-a8c7-bfcb202d33ee

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

spydon added 4 commits August 5, 2026 17:09
The four fetch layers each rebuilt the same primitives. They now come from
`supabase_common`:

- `sendRequest`, which sends over the caller's `Client` or a one-off one.
  Used by functions, storage's three send sites and the Iceberg catalog.
- `headerValue`, a case-insensitive header lookup.
- `setDefaultContentType`, which only sets `Content-Type` when the caller
  did not, replacing storage's and functions' own case-insensitive checks.
- `responseMediaType`, replacing functions' manual parse of the response
  content type and the Iceberg catalog's `contains('application/json')`.
- `tryDecodeJsonObject`, for error bodies that a proxy or gateway may
  return as something other than a JSON object.

Two bugs fall out of the last one:

- Storage cast the decoded error body to `Map<String, dynamic>` inside a
  `try`/`on FormatException`, so a JSON body that parsed but was not an
  object (an array, say) escaped as a `TypeError` instead of a
  `StorageException`.
- Postgrest wrapped both the decode and the `maybeSingle` handling in one
  `try`/`catch (_)`, so the typed exception the handler rethrows for a real
  error was swallowed and replaced by a generic one built from the raw body,
  losing the reported code, details and hint.

Postgrest's own send site keeps managing its `Client` explicitly, since it
has to close a client it created even when the response body fails midway,
which `BaseRequest.send` does not do.

Part of #1572 (tier 3), under the v3 umbrella #1278.
Removing the catch-all around the postgrest error decode exposed the next
layer of the same problem: `PostgrestException.fromJson` cast `code` and
`hint` to `String` and required a `String` message, so an error body that is
a JSON object with different field types, say the `{"code": 502, "message":
"Bad gateway"}` a gateway returns, threw a `TypeError` out of the builder
instead of surfacing as a `PostgrestException`.

Both factories now read every field defensively, so any JSON object produces
an exception. `StorageException.fromJson` had the same casts and the same
exposure.

Part of #1572 (tier 3), under the v3 umbrella #1278.
@spydon
spydon force-pushed the breaking/shared-exponential-backoff branch from c2b1b6c to 549c593 Compare August 5, 2026 15:15
@spydon
spydon force-pushed the refactor/shared-http-fetch-pieces branch from 5a5a1fa to c3cfc32 Compare August 5, 2026 15:15
@spydon spydon added the v3 label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants