diff --git a/packages/powersync_core/lib/src/exceptions.dart b/packages/powersync_core/lib/src/exceptions.dart index e4f7c864..bc35df4a 100644 --- a/packages/powersync_core/lib/src/exceptions.dart +++ b/packages/powersync_core/lib/src/exceptions.dart @@ -61,7 +61,12 @@ class SyncResponseException implements Exception { static SyncResponseException _fromResponseBody( http.BaseResponse response, String body) { final decoded = convert.jsonDecode(body); - final details = _stringOrFirst(decoded['error']?['details']) ?? body; + final details = switch (decoded['error']) { + final Map details => _errorDescription(details), + _ => null, + } ?? + body; + final message = '${response.reasonPhrase ?? "Request failed"}: $details'; return SyncResponseException(response.statusCode, message); } @@ -73,6 +78,37 @@ class SyncResponseException implements Exception { ); } + /// Extracts an error description from an error resonse looking like + /// `{"code":"PSYNC_S2106","status":401,"description":"Authentication required","name":"AuthorizationError"}`. + static String? _errorDescription(Map raw) { + final code = raw['code']; // Required, string + final description = raw['description']; // Required, string + + final name = raw['name']; // Optional, string + final details = raw['details']; // Optional, string + + if (code is! String || description is! String) { + return null; + } + + final fullDescription = StringBuffer(code); + if (name is String) { + fullDescription.write('($name)'); + } + + fullDescription + ..write(': ') + ..write(description); + + if (details is String) { + fullDescription + ..write(', ') + ..write(details); + } + + return fullDescription.toString(); + } + int statusCode; String description; @@ -84,18 +120,6 @@ class SyncResponseException implements Exception { } } -String? _stringOrFirst(Object? details) { - if (details == null) { - return null; - } else if (details is String) { - return details; - } else if (details case [final String first, ...]) { - return first; - } else { - return null; - } -} - class PowersyncNotReadyException implements Exception { /// @nodoc PowersyncNotReadyException(this.message); diff --git a/packages/powersync_core/test/exceptions_test.dart b/packages/powersync_core/test/exceptions_test.dart new file mode 100644 index 00000000..d63a152d --- /dev/null +++ b/packages/powersync_core/test/exceptions_test.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:powersync_core/src/exceptions.dart'; +import 'package:test/test.dart'; + +void main() { + group('SyncResponseException', () { + const errorResponse = + '{"error":{"code":"PSYNC_S2106","status":401,"description":"Authentication required","name":"AuthorizationError"}}'; + + test('fromStreamedResponse', () async { + final exc = await SyncResponseException.fromStreamedResponse( + StreamedResponse(Stream.value(utf8.encode(errorResponse)), 401)); + + expect(exc.statusCode, 401); + expect(exc.description, + 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required'); + }); + + test('fromResponse', () { + final exc = + SyncResponseException.fromResponse(Response(errorResponse, 401)); + expect(exc.statusCode, 401); + expect(exc.description, + 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required'); + }); + + test('with description', () { + const errorResponse = + '{"error":{"code":"PSYNC_S2106","status":401,"description":"Authentication required","name":"AuthorizationError", "details": "Missing authorization header"}}'; + + final exc = + SyncResponseException.fromResponse(Response(errorResponse, 401)); + expect(exc.statusCode, 401); + expect(exc.description, + 'Request failed: PSYNC_S2106(AuthorizationError): Authentication required, Missing authorization header'); + }); + + test('malformed', () { + const malformed = + '{"message":"Route GET:/foo/bar not found","error":"Not Found","statusCode":404}'; + + final exc = SyncResponseException.fromResponse(Response(malformed, 401)); + expect(exc.statusCode, 401); + expect(exc.description, + 'Request failed: {"message":"Route GET:/foo/bar not found","error":"Not Found","statusCode":404}'); + + final exc2 = SyncResponseException.fromResponse(Response( + 'not even json', 500, + reasonPhrase: 'Internal server error')); + expect(exc2.statusCode, 500); + expect(exc2.description, 'Internal server error'); + }); + }); +}