fix(runtime): harden application service boundaries

This commit is contained in:
edde746
2026-07-24 03:46:46 +02:00
parent 658da37b48
commit e0bf66eea8
309 changed files with 32574 additions and 4369 deletions
@@ -425,12 +425,16 @@ class AnilistClient implements DisposableTrackerClient {
);
}
if (res.statusCode != 200) {
throw TrackerApiException(service: TrackerService.anilist, statusCode: res.statusCode, body: res.body);
throw TrackerApiException(service: TrackerService.anilist, statusCode: res.statusCode);
}
final decoded = json.decode(res.body) as Map<String, dynamic>;
final errors = decoded['errors'];
if (errors is List && errors.isNotEmpty) {
throw TrackerApiException(service: TrackerService.anilist, statusCode: res.statusCode, body: json.encode(errors));
throw TrackerApiException(
service: TrackerService.anilist,
statusCode: res.statusCode,
category: TrackerApiFailureCategory.graphqlErrors,
);
}
final data = decoded['data'];
return data is Map ? data.cast<String, dynamic>() : <String, dynamic>{};
@@ -56,7 +56,7 @@ class MalAuthService extends OAuthProxyAuthServiceBase {
);
if (res.statusCode != 200) {
appLogger.w('MAL: refresh failed (${res.statusCode}): ${res.body}');
appLogger.w('MAL: refresh failed (HTTP ${res.statusCode})');
throw TrackerAuthException(
service: TrackerService.mal,
message: 'Refresh failed: HTTP ${res.statusCode}',
+2 -2
View File
@@ -193,7 +193,7 @@ class MalClient implements DisposableTrackerClient {
try {
await _refresh();
} catch (_) {
throw TrackerApiException(service: TrackerService.mal, statusCode: 401, body: res.body);
throw const TrackerApiException(service: TrackerService.mal, statusCode: 401);
}
res = await _send(method, path, body: body, formBody: formBody);
}
@@ -201,7 +201,7 @@ class MalClient implements DisposableTrackerClient {
if (res.statusCode >= 200 && res.statusCode < 300) {
return TrackerHttpClient.decodeJson(res.body);
}
throw TrackerApiException(service: TrackerService.mal, statusCode: res.statusCode, body: res.body);
throw TrackerApiException(service: TrackerService.mal, statusCode: res.statusCode);
}
Future<http.Response> _send(
@@ -40,7 +40,7 @@ class OAuthProxyClient {
operation: 'OAuth proxy start',
);
if (res.statusCode != 200) {
throw OAuthProxyException('start failed: HTTP ${res.statusCode}: ${res.body}');
throw OAuthProxyException('OAuth proxy start failed: HTTP ${res.statusCode}');
}
final body = json.decode(res.body) as Map<String, dynamic>;
return OAuthProxyStart(
@@ -94,13 +94,18 @@ class OAuthProxyClient {
throw const OAuthProxyException('Session expired or already used');
}
if (res.statusCode != 200) {
throw OAuthProxyException('poll failed: HTTP ${res.statusCode}: ${res.body}');
throw OAuthProxyException('OAuth proxy poll failed: HTTP ${res.statusCode}');
}
final body = json.decode(res.body) as Map<String, dynamic>;
if (body['error'] != null) {
final err = body['error'] as String;
final err = body['error'];
if (err == 'access_denied') return null; // user cancelled in browser
throw OAuthProxyException('Upstream auth failed: $err');
final category = switch (err) {
'missing_code' => 'missing authorization code',
'exchange_failed' => 'token exchange failed',
_ => 'upstream authorization failed',
};
throw OAuthProxyException('OAuth proxy failed: $category');
}
return OAuthProxyResult(
accessToken: body['accessToken'] as String,
@@ -34,7 +34,7 @@ class SimklAuthService extends DeviceCodeAuthServiceBase {
operation: 'Simkl PIN request',
);
if (res.statusCode != 200) {
throw DeviceCodeAuthFlowException('Simkl PIN request failed: HTTP ${res.statusCode}: ${res.body}');
throw DeviceCodeAuthFlowException('Simkl PIN request failed: HTTP ${res.statusCode}');
}
final body = json.decode(res.body) as Map<String, dynamic>;
return DeviceCode(
@@ -164,7 +164,7 @@ class SimklClient implements DisposableTrackerClient {
);
}
if (response.statusCode < 200 || response.statusCode >= 300) {
throw TrackerApiException(service: TrackerService.simkl, statusCode: response.statusCode, body: response.body);
throw TrackerApiException(service: TrackerService.simkl, statusCode: response.statusCode);
}
return response;
}
@@ -1,14 +1,19 @@
import 'tracker_constants.dart';
enum TrackerApiFailureCategory { graphqlErrors }
class TrackerApiException implements Exception {
final TrackerService service;
final int statusCode;
final String body;
final TrackerApiFailureCategory? category;
const TrackerApiException({required this.service, required this.statusCode, required this.body});
const TrackerApiException({required this.service, required this.statusCode, this.category});
@override
String toString() => 'TrackerApiException(${service.name}, HTTP $statusCode): $body';
String toString() {
final categorySuffix = category == null ? '' : ', ${category!.name}';
return 'TrackerApiException(${service.name}, HTTP $statusCode$categorySuffix)';
}
}
class TrackerAuthException implements Exception {