From 8ec202fea2e44ec21aa6e084547411d46f2c2c61 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 22 Jul 2025 09:57:31 +0200 Subject: [PATCH 1/3] Fix parsing errors --- .../powersync_core/lib/src/exceptions.dart | 50 ++++++++++++++----- .../powersync_core/test/exceptions_test.dart | 39 +++++++++++++++ 2 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 packages/powersync_core/test/exceptions_test.dart diff --git a/packages/powersync_core/lib/src/exceptions.dart b/packages/powersync_core/lib/src/exceptions.dart index e4f7c864..ab5feffc 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..6c9f883b --- /dev/null +++ b/packages/powersync_core/test/exceptions_test.dart @@ -0,0 +1,39 @@ +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('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}'); + }); + }); +} From 4abac9e20e6ff9ee4f64ccb3ce2e9614d8424572 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 22 Jul 2025 10:01:53 +0200 Subject: [PATCH 2/3] Another test for recovering from header --- packages/powersync_core/test/exceptions_test.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/powersync_core/test/exceptions_test.dart b/packages/powersync_core/test/exceptions_test.dart index 6c9f883b..a7fe96e3 100644 --- a/packages/powersync_core/test/exceptions_test.dart +++ b/packages/powersync_core/test/exceptions_test.dart @@ -34,6 +34,12 @@ void main() { 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'); }); }); } From ed64d6099f502c984d50836d3cbae3e683ebb12f Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 22 Jul 2025 10:03:29 +0200 Subject: [PATCH 3/3] Another one on parsing details --- packages/powersync_core/lib/src/exceptions.dart | 2 +- packages/powersync_core/test/exceptions_test.dart | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/powersync_core/lib/src/exceptions.dart b/packages/powersync_core/lib/src/exceptions.dart index ab5feffc..bc35df4a 100644 --- a/packages/powersync_core/lib/src/exceptions.dart +++ b/packages/powersync_core/lib/src/exceptions.dart @@ -102,7 +102,7 @@ class SyncResponseException implements Exception { if (details is String) { fullDescription - ..write(' ') + ..write(', ') ..write(details); } diff --git a/packages/powersync_core/test/exceptions_test.dart b/packages/powersync_core/test/exceptions_test.dart index a7fe96e3..d63a152d 100644 --- a/packages/powersync_core/test/exceptions_test.dart +++ b/packages/powersync_core/test/exceptions_test.dart @@ -26,6 +26,17 @@ void main() { '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}';