feat: enhance API error handling and response structure; introduce ApiException for V2 error codes
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user