Skip to content

refactor!: give every service exception a shared SupabaseException base - #1644

Open
spydon wants to merge 7 commits into
mainfrom
breaking/supabase-exception-base
Open

refactor!: give every service exception a shared SupabaseException base#1644
spydon wants to merge 7 commits into
mainfrom
breaking/supabase-exception-base

Conversation

@spydon

@spydon spydon commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Tier 3 of #1572, first of four PRs. Under the v3 umbrella #1278.

What

AuthException, StorageException, PostgrestException and FunctionException each reimplemented the same "message plus status" shape under different field names and types. They now extend one SupabaseException in supabase_common:

abstract class SupabaseException implements Exception {
  final String message;
  final int? statusCode;   // null when no response was received
  final String? errorCode; // service specific, e.g. weak_password, PGRST116, not_found
}

Every package re-exports SupabaseException, so a single catch handles a failure from any service:

try {
  await supabase.from('countries').select();
} on SupabaseException catch (error) {
  print('${error.statusCode}: ${error.message}');
}

Breaking changes

Before After
AuthException.statusCode (String?) statusCode (int?)
AuthException.code AuthException.errorCode
StorageException.statusCode (String?) statusCode (int?)
StorageException.error StorageException.errorCode
StorageException.fromJson(json, '404') StorageException.fromJson(json, 404)
PostgrestException.code (PostgREST code, or the HTTP status when the body was not JSON) errorCode (PostgREST/PostgreSQL code only) and statusCode (HTTP status)
PostgrestException.fromJson(json, code: 409) PostgrestException.fromJson(json, statusCode: 409)
PostgrestException.toJson() keys code keys statusCode and errorCode
FunctionException.status (int) statusCode (int?)
FunctionException.reasonPhrase folded into message
FunctionsFetchException.status == 0 statusCode == null
FunctionResponse.status FunctionResponse.statusCode

Notes on the less mechanical ones:

  • Postgrest no longer overloads code. It used to stuff the HTTP status into code when the error body was not JSON, so code was sometimes PGRST116 and sometimes 409. The status now has its own field and errorCode is only ever a PostgREST/PostgreSQL code. A duplicate-key error, for example, is now statusCode: 409, errorCode: '23505'.
  • Functions gained a message. FunctionException had no message, only status, details and reasonPhrase. The response's reason phrase becomes the message, and when the response carries none (as over HTTP/2) each subtype falls back to its own default, matching supabase-js: 'Failed to send a request to the Edge Function', 'Relay error invoking the Edge Function', 'Edge Function returned a non-2xx status code'. The response body is still in details.
  • FunctionResponse.status is renamed too. With the exceptions in that package reporting statusCode, the successful response was the only thing left calling it status. It also lines up with http.Response.statusCode.

Auth's getSessionFromUrl also had to change how it reads an error callback: error_code holds either a numeric status (older links) or a code such as otp_expired, so the numeric form becomes statusCode and anything else becomes errorCode, falling back to the error parameter. Both shapes are covered by tests.

Every auth exception subclass used to repeat the same toString; the base now prints the concrete runtime type, so only the subtypes with extra fields (originalError, reasons, details, hint) override it.

A bug this surfaces

Fetch._handleError in storage cast the decoded error body to
Map<String, dynamic> inside a try/on FormatException. A body that parses
as JSON but is not an object, for example the ["upstream connect error"] a
gateway can return, throws a TypeError on that cast, which the
on FormatException does not catch, so it escaped instead of surfacing as a
StorageException. Fixed here, with a regression test; #1647 later replaces the
inline guard with the shared tryDecodeJsonObject helper.

Out of scope

RealtimeSubscribeException and the sealed IcebergException keep their own shapes for now. Making FunctionException sealed is tracked separately in #1550.

Testing

melos analyze, dcm analyze and melos format clean. Full test suites pass for gotrue, postgrest, storage_client, realtime_client, supabase, supabase_common (against the local stack) and supabase_flutter, plus the examples analyzer. The capability matrix symbol and drift checks pass unchanged.

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

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds SupabaseException in the shared package. It migrates Functions, GoTrue, PostgREST, and Storage exceptions to use statusCode and errorCode. It also updates parsing, exports, examples, SDK compliance, and tests.

Changes

Shared exception contract

Layer / File(s) Summary
Shared exception base
packages/supabase_common/...
Adds and exports SupabaseException with message, nullable statusCode, nullable errorCode, and formatted toString() output.

Authentication exceptions

Layer / File(s) Summary
GoTrue exception migration
packages/gotrue/lib/...
Migrates authentication exceptions to SupabaseException, renames code to errorCode, and uses integer HTTP status codes.
GoTrue validation and integration updates
packages/gotrue/test/..., packages/supabase_flutter/test/deep_link_test.dart
Updates authentication assertions, equality checks, string output checks, OAuth error matching, and named error-code coverage.

Functions exceptions

Layer / File(s) Summary
Functions exception migration
packages/functions_client/lib/...
Migrates function exceptions to SupabaseException, uses statusCode and message, and represents transport failures with no response status.
Functions examples, compliance, and tests
packages/functions_client/test/..., packages/functions_client/example/..., examples/edge_functions/..., sdk-compliance.yaml
Updates function assertions, fallback messages, examples, comments, and invocation symbol mappings.

PostgREST exceptions

Layer / File(s) Summary
PostgREST exception migration
packages/postgrest/lib/..., packages/postgrest/example/...
Migrates PostgrestException to SupabaseException and separates HTTP statusCode from PostgreSQL errorCode.
PostgREST validation
packages/postgrest/test/...
Updates response, maybeSingle, RPC, malformed-response, and upsert assertions.

Storage exceptions

Layer / File(s) Summary
Storage exception migration
packages/storage_client/lib/...
Migrates StorageException to SupabaseException, preserves integer HTTP status codes, and maps JSON errors to errorCode.
Storage validation
packages/storage_client/test/...
Updates storage status-code, error-code, hierarchy, parsing, and cleanup assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested labels: storage

Suggested reviewers: tr00d

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main breaking change: introducing a shared SupabaseException base for service exceptions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch breaking/supabase-exception-base

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
examples/edge_functions/lib/main.dart (1)

363-367: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle transport failures without rendering a null status.

When a request fails before a response, statusCode is null. The current fallback displays Function failed with status null. Use error.message for that case.

Proposed fix
-        : 'Function failed with status ${error.statusCode}';
+        : error.statusCode == null
+            ? error.message
+            : 'Function failed with status ${error.statusCode}';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/edge_functions/lib/main.dart` around lines 363 - 367, Update the
FunctionException fallback in the error-handling flow to use error.message when
error.statusCode is null, avoiding a rendered “status null” message. Preserve
the existing details['error'] handling and continue using the status-based
fallback when a status code is available.
packages/postgrest/lib/src/types.dart (1)

27-49: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve normalized fields in PostgrestException.fromJson().

toJson() writes statusCode and errorCode, but the constructor only passes those fields in through separate arguments. When an app serializes an example exception, then deserializes the JSON with PostgrestException.fromJson(...), statusCode and errorCode disappear. Add fallback reads for json['statusCode'] and json['errorCode'], and cover this in a round-trip test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/postgrest/lib/src/types.dart` around lines 27 - 49, Update
PostgrestException.fromJson() to use json['statusCode'] and json['errorCode'] as
fallbacks when the statusCode and errorCode arguments are absent, preserving
explicitly supplied arguments. Add a round-trip test covering toJson() followed
by fromJson() and verifying both normalized fields remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/storage_client/lib/src/fetch.dart`:
- Around line 40-50: Update the error-body parsing flow around
StorageException.fromJson so valid non-object JSON values, including arrays,
strings, and null, are normalized to a StorageException containing the raw
error.body instead of triggering an uncaught TypeError; preserve the existing
object parsing and FormatException behavior, and add a regression test covering
a non-object JSON HTTP error response.

In `@packages/supabase_common/lib/src/supabase_exception.dart`:
- Around line 3-5: Correct the exception-contract documentation so it applies
only to exceptions extending SupabaseException, rather than every service
exception. Update the documentation comment in
packages/supabase_common/lib/src/supabase_exception.dart at lines 3-5 and the
inheritance statement in packages/supabase_common/README.md at lines 14-16;
explicitly scope the README statement to the migrated exception types and leave
RealtimeSubscribeException and IcebergException excluded.

---

Outside diff comments:
In `@examples/edge_functions/lib/main.dart`:
- Around line 363-367: Update the FunctionException fallback in the
error-handling flow to use error.message when error.statusCode is null, avoiding
a rendered “status null” message. Preserve the existing details['error']
handling and continue using the status-based fallback when a status code is
available.

In `@packages/postgrest/lib/src/types.dart`:
- Around line 27-49: Update PostgrestException.fromJson() to use
json['statusCode'] and json['errorCode'] as fallbacks when the statusCode and
errorCode arguments are absent, preserving explicitly supplied arguments. Add a
round-trip test covering toJson() followed by fromJson() and verifying both
normalized fields remain unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1746a526-08a7-4060-8c83-db9eeecb782a

📥 Commits

Reviewing files that changed from the base of the PR and between 5be5a16 and 192d159.

📒 Files selected for processing (39)
  • examples/edge_functions/integration_test/functions_test.dart
  • examples/edge_functions/integration_test/invoke_test.dart
  • examples/edge_functions/lib/main.dart
  • packages/functions_client/example/functions_dart_example.dart
  • packages/functions_client/lib/functions_client.dart
  • packages/functions_client/lib/src/functions_client.dart
  • packages/functions_client/lib/src/types.dart
  • packages/functions_client/test/functions_dart_test.dart
  • packages/gotrue/lib/gotrue.dart
  • packages/gotrue/lib/src/fetch.dart
  • packages/gotrue/lib/src/gotrue_client.dart
  • packages/gotrue/lib/src/types/auth_exception.dart
  • packages/gotrue/test/client_test.dart
  • packages/gotrue/test/fetch_test.dart
  • packages/gotrue/test/otp_mock_test.dart
  • packages/gotrue/test/passkey_test.dart
  • packages/gotrue/test/provider_test.dart
  • packages/gotrue/test/src/gotrue_oauth_api_test.dart
  • packages/gotrue/test/src/types/auth_exception_test.dart
  • packages/gotrue/test/web3_auth_test.dart
  • packages/postgrest/example/main.dart
  • packages/postgrest/lib/postgrest.dart
  • packages/postgrest/lib/src/postgrest_builder.dart
  • packages/postgrest/lib/src/types.dart
  • packages/postgrest/test/basic_test.dart
  • packages/postgrest/test/transforms_test.dart
  • packages/postgrest/test/upsert_test.dart
  • packages/storage_client/lib/src/fetch.dart
  • packages/storage_client/lib/src/storage_file_api.dart
  • packages/storage_client/lib/src/types.dart
  • packages/storage_client/lib/storage_client.dart
  • packages/storage_client/test/basic_test.dart
  • packages/storage_client/test/client_test.dart
  • packages/storage_client/test/types_test.dart
  • packages/supabase_common/README.md
  • packages/supabase_common/lib/src/supabase_exception.dart
  • packages/supabase_common/lib/supabase_common.dart
  • packages/supabase_common/test/supabase_exception_test.dart
  • packages/supabase_flutter/test/deep_link_test.dart

Comment thread packages/storage_client/lib/src/fetch.dart Outdated
Comment thread packages/supabase_common/lib/src/supabase_exception.dart Outdated
@spydon spydon changed the title breaking: give every service exception a shared SupabaseException base refactor!: give every service exception a shared SupabaseException base Aug 5, 2026
spydon added 7 commits August 5, 2026 17:06
`AuthException`, `StorageException`, `PostgrestException` and
`FunctionException` each reimplemented the same message plus status shape
under different field names and types. They now extend a single
`SupabaseException` in `supabase_common`, so `on SupabaseException` catches
a failure from any service.

Reconciling the four shapes means:

- `statusCode` is an `int?` everywhere. It was a `String?` in auth and
  storage, and `FunctionException.status` (an `int`) in functions.
- The service specific code is `errorCode` everywhere: `AuthException.code`,
  `StorageException.error` and `PostgrestException.code` are gone.
  `PostgrestException.errorCode` now only holds the PostgREST/PostgreSQL
  code, since the HTTP status has its own field.
- `PostgrestException.fromJson` takes `statusCode` instead of `code`.
- `FunctionException` carries a `message` like the other exceptions.
  `reasonPhrase` is gone: the response's reason phrase becomes the message,
  falling back to a per-subtype default when the response has none.
- `FunctionsFetchException.statusCode` is `null` instead of `0`, since no
  response reached the client.
- Auth exception subclasses no longer each repeat `toString`; the base
  prints the concrete runtime type.

Part of #1572 (tier 3), under the v3 umbrella #1278.
The exceptions in this package now carry the HTTP status as `statusCode`,
so the successful response reporting it as `status` was the odd one out.
It also lines up with `http.Response.statusCode`.
…ypes

The base class doc and the README claimed every service exception extends
`SupabaseException`, but `RealtimeSubscribeException` and `IcebergException`
deliberately keep shapes of their own.
`_handleError` cast the decoded error body to `Map<String, dynamic>` inside a
`try`/`on FormatException`. A body that parses as JSON but is not an object,
for example the `["upstream connect error"]` a gateway can return, throws a
`TypeError` on that cast, which the `on FormatException` does not catch, so it
escaped instead of surfacing as a `StorageException`.

The non-response branch also moved up front, which drops a level of nesting.
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