fix(runtime): harden application service boundaries
This commit is contained in:
@@ -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}',
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user