Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,32 @@ Four changes go beyond a rename:
`RealtimeSubscribeException` is not part of this hierarchy: it reports a channel subscription
outcome rather than a request failure, and carries a `RealtimeSubscribeStatus` instead of a message.

### `FunctionException` is sealed

`FunctionException` is now a `sealed class`, so a switch over it is exhaustive at compile time and
adding a failure mode in a later version is a compile error rather than a case that silently falls
through:

```dart
try {
await supabase.functions.invoke('hello');
} on FunctionException catch (error) {
final message = switch (error) {
FunctionsFetchException() => 'The request never reached the function',
FunctionsRelayException() => 'The relay reported an error',
FunctionsApiException() => 'The function returned ${error.statusCode}',
};
}
```

`FunctionsRelayException` extends `FunctionsApiException`, so it has to come first for its case to
be reachable. Only `FunctionsFetchException` and `FunctionsApiException` are needed for the switch
to be exhaustive.

`sealed` implies `abstract`, so a bare `FunctionException` can no longer be constructed, and code
outside `supabase_functions` can no longer extend or implement it. Name one of the three subtypes
instead, which is what the client throws in every case.

### The Iceberg exceptions join the same hierarchy

`IcebergException` used `0` as the status code when no response was received, so callers
Expand Down
12 changes: 8 additions & 4 deletions packages/supabase_functions/lib/src/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,14 @@ class FunctionResponse {
/// The response body, or the originating error when no response was received,
/// is available in [details].
///
/// A plain [FunctionException] is a failure the client raised on its own, such
/// as a request that never reached the function. A failure the function
/// answered with is a [FunctionsApiException].
class FunctionException extends SupabaseException {
/// Use pattern matching over the specific subtypes:
/// - [FunctionsFetchException]: The request could not be sent (e.g. network
/// failure).
/// - [FunctionsApiException]: The Edge Function answered with a non-2xx status
/// code.
/// - [FunctionsRelayException]: The Supabase relay returned an error
/// (`x-relay-error`).
sealed class FunctionException extends SupabaseException {
final dynamic details;

const FunctionException({
Expand Down
18 changes: 18 additions & 0 deletions packages/supabase_functions/test/functions_dart_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,24 @@ void main() {
);
});

test('exhaustive switch over FunctionException subtypes', () {
String describeError(FunctionException exception) {
return switch (exception) {
FunctionsFetchException(:final message) => 'fetch: $message',
FunctionsRelayException(:final statusCode) => 'relay: $statusCode',
FunctionsApiException(:final statusCode) => 'api: $statusCode',
};
}

const fetchError = FunctionsFetchException(message: 'Connection failed');
const relayError = FunctionsRelayException(statusCode: 500);
const apiError = FunctionsApiException(statusCode: 400);

expect(describeError(fetchError), 'fetch: Connection failed');
expect(describeError(relayError), 'relay: 500');
expect(describeError(apiError), 'api: 400');
});

test(
'error response with a streaming content type exposes the body',
() async {
Expand Down