2026-03-18 17:03:22 +08:00
|
|
|
import 'dart:convert';
|
|
|
|
|
|
2026-02-25 18:00:02 +08:00
|
|
|
import 'package:dio/dio.dart';
|
2026-03-29 20:26:30 +08:00
|
|
|
import '../../core/l10n/l10n.dart';
|
2026-03-27 14:05:03 +08:00
|
|
|
import 'error_code_mapper.dart';
|
2026-02-25 18:00:02 +08:00
|
|
|
|
2026-02-25 14:36:03 +08:00
|
|
|
abstract class ApiException implements Exception {
|
|
|
|
|
final String message;
|
|
|
|
|
final int? statusCode;
|
2026-03-27 14:05:03 +08:00
|
|
|
final String? errorCode;
|
|
|
|
|
final Map<String, dynamic>? errorParams;
|
2026-02-25 14:36:03 +08:00
|
|
|
|
2026-03-27 14:05:03 +08:00
|
|
|
const ApiException(
|
|
|
|
|
this.message, {
|
|
|
|
|
this.statusCode,
|
|
|
|
|
this.errorCode,
|
|
|
|
|
this.errorParams,
|
|
|
|
|
});
|
2026-02-25 14:36:03 +08:00
|
|
|
|
2026-03-11 20:51:56 +08:00
|
|
|
@override
|
|
|
|
|
String toString() => message;
|
|
|
|
|
|
2026-02-25 14:36:03 +08:00
|
|
|
factory ApiException.fromDioError(Object error) {
|
|
|
|
|
if (error is ApiException) return error;
|
2026-02-25 18:00:02 +08:00
|
|
|
if (error is DioException) {
|
|
|
|
|
final response = error.response;
|
|
|
|
|
final statusCode = response?.statusCode;
|
|
|
|
|
final data = response?.data;
|
|
|
|
|
|
|
|
|
|
String detail;
|
2026-03-27 14:05:03 +08:00
|
|
|
String? errorCode;
|
|
|
|
|
Map<String, dynamic>? errorParams;
|
2026-03-18 17:03:22 +08:00
|
|
|
final decodedData = _normalizeErrorData(data);
|
|
|
|
|
|
|
|
|
|
if (decodedData is Map<String, dynamic>) {
|
2026-02-25 18:00:02 +08:00
|
|
|
detail =
|
2026-03-18 17:03:22 +08:00
|
|
|
(decodedData['detail'] ??
|
|
|
|
|
decodedData['message'] ??
|
|
|
|
|
decodedData['error'])
|
|
|
|
|
?.toString() ??
|
2026-03-27 14:05:03 +08:00
|
|
|
L10n.current.errorRequestFailed;
|
|
|
|
|
final code = decodedData['code'];
|
|
|
|
|
if (code is String && code.trim().isNotEmpty) {
|
|
|
|
|
errorCode = code;
|
|
|
|
|
}
|
|
|
|
|
final params = decodedData['params'];
|
|
|
|
|
if (params is Map<String, dynamic>) {
|
|
|
|
|
errorParams = params;
|
|
|
|
|
} else if (params is Map) {
|
|
|
|
|
errorParams = params.map(
|
|
|
|
|
(key, value) => MapEntry(key.toString(), value),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-25 18:00:02 +08:00
|
|
|
} else {
|
2026-03-17 00:13:41 +08:00
|
|
|
detail = _networkErrorMessage(error);
|
2026-02-25 18:00:02 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 14:05:03 +08:00
|
|
|
final localized = _localizeError(
|
|
|
|
|
detail,
|
|
|
|
|
statusCode,
|
|
|
|
|
errorCode: errorCode,
|
|
|
|
|
errorParams: errorParams,
|
|
|
|
|
);
|
2026-02-25 18:00:02 +08:00
|
|
|
|
|
|
|
|
if (statusCode == 401) {
|
2026-03-27 14:05:03 +08:00
|
|
|
return UnauthorizedException(message: localized, errorCode: errorCode);
|
2026-02-25 18:00:02 +08:00
|
|
|
}
|
|
|
|
|
if (statusCode == 422) {
|
|
|
|
|
return ValidationException(
|
|
|
|
|
localized,
|
|
|
|
|
errors: data,
|
|
|
|
|
statusCode: statusCode,
|
2026-03-27 14:05:03 +08:00
|
|
|
errorCode: errorCode,
|
|
|
|
|
errorParams: errorParams,
|
2026-02-25 18:00:02 +08:00
|
|
|
);
|
|
|
|
|
}
|
2026-03-27 14:05:03 +08:00
|
|
|
return ServerException(
|
|
|
|
|
localized,
|
|
|
|
|
statusCode: statusCode,
|
|
|
|
|
errorCode: errorCode,
|
|
|
|
|
errorParams: errorParams,
|
|
|
|
|
);
|
2026-02-25 18:00:02 +08:00
|
|
|
}
|
2026-03-27 14:05:03 +08:00
|
|
|
return ServerException(L10n.current.errorNetwork);
|
2026-02-25 14:36:03 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-18 17:03:22 +08:00
|
|
|
static Map<String, dynamic>? _normalizeErrorData(dynamic data) {
|
|
|
|
|
if (data is Map<String, dynamic>) {
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
if (data is Map) {
|
|
|
|
|
return data.map((key, value) => MapEntry(key.toString(), value));
|
|
|
|
|
}
|
|
|
|
|
if (data is String && data.trim().isNotEmpty) {
|
|
|
|
|
try {
|
|
|
|
|
final decoded = jsonDecode(data);
|
|
|
|
|
if (decoded is Map<String, dynamic>) {
|
|
|
|
|
return decoded;
|
|
|
|
|
}
|
|
|
|
|
if (decoded is Map) {
|
|
|
|
|
return decoded.map((key, value) => MapEntry(key.toString(), value));
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 14:05:03 +08:00
|
|
|
static String _localizeError(
|
|
|
|
|
String detail,
|
|
|
|
|
int? statusCode, {
|
|
|
|
|
String? errorCode,
|
|
|
|
|
Map<String, dynamic>? errorParams,
|
|
|
|
|
}) {
|
|
|
|
|
final mapped = resolveErrorCodeMessage(errorCode, params: errorParams);
|
|
|
|
|
if (mapped != null && mapped.isNotEmpty) {
|
|
|
|
|
return mapped;
|
|
|
|
|
}
|
2026-02-25 18:00:02 +08:00
|
|
|
if (statusCode == 403) {
|
2026-03-27 14:05:03 +08:00
|
|
|
return L10n.current.errorForbidden;
|
2026-02-25 18:00:02 +08:00
|
|
|
}
|
|
|
|
|
if (statusCode == 404) {
|
2026-03-27 14:05:03 +08:00
|
|
|
return L10n.current.errorNotFound;
|
2026-02-25 18:00:02 +08:00
|
|
|
}
|
|
|
|
|
if (statusCode == 429) {
|
2026-03-27 14:05:03 +08:00
|
|
|
return L10n.current.errorTooManyRequests;
|
2026-02-25 18:00:02 +08:00
|
|
|
}
|
|
|
|
|
if (statusCode != null && statusCode >= 500) {
|
2026-03-27 14:05:03 +08:00
|
|
|
return L10n.current.errorServer;
|
2026-02-25 18:00:02 +08:00
|
|
|
}
|
2026-03-27 14:05:03 +08:00
|
|
|
return L10n.current.errorGenericSafe;
|
2026-02-25 18:00:02 +08:00
|
|
|
}
|
2026-03-17 00:13:41 +08:00
|
|
|
|
|
|
|
|
static String _networkErrorMessage(DioException error) {
|
|
|
|
|
if (error.type == DioExceptionType.connectionTimeout ||
|
|
|
|
|
error.type == DioExceptionType.sendTimeout ||
|
|
|
|
|
error.type == DioExceptionType.receiveTimeout) {
|
2026-03-27 14:05:03 +08:00
|
|
|
return L10n.current.errorNetworkTimeout;
|
2026-03-17 00:13:41 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error.type == DioExceptionType.connectionError ||
|
|
|
|
|
error.type == DioExceptionType.unknown) {
|
2026-03-27 14:05:03 +08:00
|
|
|
return L10n.current.errorNetworkUnavailable;
|
2026-03-17 00:13:41 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 14:05:03 +08:00
|
|
|
return L10n.current.errorRequestFailed;
|
2026-03-17 00:13:41 +08:00
|
|
|
}
|
2026-02-25 14:36:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ServerException extends ApiException {
|
2026-03-27 14:05:03 +08:00
|
|
|
const ServerException(
|
|
|
|
|
super.message, {
|
|
|
|
|
super.statusCode,
|
|
|
|
|
super.errorCode,
|
|
|
|
|
super.errorParams,
|
|
|
|
|
});
|
2026-02-25 14:36:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class UnauthorizedException extends ApiException {
|
2026-03-27 14:05:03 +08:00
|
|
|
UnauthorizedException({String? message, String? errorCode})
|
|
|
|
|
: super(
|
|
|
|
|
message ?? L10n.current.errorReLogin,
|
|
|
|
|
statusCode: 401,
|
|
|
|
|
errorCode: errorCode,
|
|
|
|
|
);
|
2026-02-25 14:36:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ValidationException extends ApiException {
|
|
|
|
|
final Map<String, dynamic>? errors;
|
2026-03-27 14:05:03 +08:00
|
|
|
const ValidationException(
|
|
|
|
|
super.message, {
|
|
|
|
|
this.errors,
|
|
|
|
|
super.statusCode,
|
|
|
|
|
super.errorCode,
|
|
|
|
|
super.errorParams,
|
|
|
|
|
});
|
2026-02-25 14:36:03 +08:00
|
|
|
}
|