feat: enhance API error handling and response structure; introduce ApiException for V2 error codes

This commit is contained in:
Achintha Isuru
2026-03-17 14:15:26 -04:00
parent d6ddb7829e
commit b6a655a261
5 changed files with 116 additions and 52 deletions

View File

@@ -5,7 +5,9 @@ import 'feature_gate.dart';
/// A service that handles HTTP communication using the [Dio] client.
///
/// Integrates [FeatureGate] to validate endpoint scopes before each request.
/// Integrates [FeatureGate] for scope validation and throws typed domain
/// exceptions ([ApiException], [NetworkException], [ServerException]) on
/// error responses so repositories never receive silent failures.
class ApiService implements BaseApiService {
/// Creates an [ApiService] with the given [Dio] instance.
ApiService(this._dio);
@@ -27,7 +29,7 @@ class ApiService implements BaseApiService {
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleError(e);
throw _mapDioException(e);
}
}
@@ -47,7 +49,7 @@ class ApiService implements BaseApiService {
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleError(e);
throw _mapDioException(e);
}
}
@@ -67,7 +69,7 @@ class ApiService implements BaseApiService {
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleError(e);
throw _mapDioException(e);
}
}
@@ -87,7 +89,7 @@ class ApiService implements BaseApiService {
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleError(e);
throw _mapDioException(e);
}
}
@@ -107,7 +109,7 @@ class ApiService implements BaseApiService {
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleError(e);
throw _mapDioException(e);
}
}
@@ -117,43 +119,63 @@ class ApiService implements BaseApiService {
/// Extracts [ApiResponse] from a successful [Response].
ApiResponse _handleResponse(Response<dynamic> response) {
final dynamic body = response.data;
final String message = body is Map<String, dynamic>
? body['message']?.toString() ?? 'Success'
: 'Success';
return ApiResponse(
code: response.statusCode?.toString() ?? '200',
message: response.data['message']?.toString() ?? 'Success',
data: response.data,
message: message,
data: body,
);
}
/// Extracts [ApiResponse] from a [DioException].
/// Maps a [DioException] to a typed domain exception.
///
/// Supports both legacy error format and V2 API error envelope
/// (`{ code, message, details, requestId }`).
ApiResponse _handleError(DioException e) {
/// The V2 API error envelope is `{ code, message, details, requestId }`.
/// This method parses it and throws the appropriate [AppException] subclass
/// so that `BlocErrorHandler` can translate it for the user.
AppException _mapDioException(DioException e) {
// Network-level failures (no response from server).
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout ||
e.type == DioExceptionType.sendTimeout ||
e.type == DioExceptionType.connectionError) {
return NetworkException(technicalMessage: e.message);
}
final int? statusCode = e.response?.statusCode;
// Parse V2 error envelope if available.
if (e.response?.data is Map<String, dynamic>) {
final Map<String, dynamic> body =
e.response!.data as Map<String, dynamic>;
return ApiResponse(
code:
body['code']?.toString() ??
e.response?.statusCode?.toString() ??
'error',
message: body['message']?.toString() ?? e.message ?? 'Error occurred',
data: body['data'] ?? body['details'],
errors: _parseErrors(body['errors']),
final String apiCode =
body['code']?.toString() ?? statusCode?.toString() ?? 'UNKNOWN';
final String apiMessage =
body['message']?.toString() ?? e.message ?? 'An error occurred';
// Map well-known codes to specific exceptions.
if (apiCode == 'UNAUTHENTICATED' || statusCode == 401) {
return NotAuthenticatedException(technicalMessage: apiMessage);
}
return ApiException(
apiCode: apiCode,
apiMessage: apiMessage,
statusCode: statusCode,
details: body['details'],
technicalMessage: '$apiCode: $apiMessage',
);
}
return ApiResponse(
code: e.response?.statusCode?.toString() ?? 'error',
message: e.message ?? 'Unknown error',
errors: <String, dynamic>{'exception': e.type.toString()},
);
}
/// Helper to parse the errors map from various possible formats.
Map<String, dynamic> _parseErrors(dynamic errors) {
if (errors is Map) {
return Map<String, dynamic>.from(errors);
// Server error without a parseable body.
if (statusCode != null && statusCode >= 500) {
return ServerException(technicalMessage: e.message);
}
return const <String, dynamic>{};
return UnknownException(technicalMessage: e.message);
}
}