Files
social-app/apps/lib/data/network/api_exception.dart
T

179 lines
4.6 KiB
Dart

import 'dart:convert';
import 'package:dio/dio.dart';
import '../../core/l10n/l10n.dart';
import 'error_code_mapper.dart';
abstract class ApiException implements Exception {
final String message;
final int? statusCode;
final String? errorCode;
final Map<String, dynamic>? errorParams;
const ApiException(
this.message, {
this.statusCode,
this.errorCode,
this.errorParams,
});
@override
String toString() => message;
factory ApiException.fromDioError(Object error) {
if (error is ApiException) return error;
if (error is DioException) {
final response = error.response;
final statusCode = response?.statusCode;
final data = response?.data;
String detail;
String? errorCode;
Map<String, dynamic>? errorParams;
final decodedData = _normalizeErrorData(data);
if (decodedData is Map<String, dynamic>) {
detail =
(decodedData['detail'] ??
decodedData['message'] ??
decodedData['error'])
?.toString() ??
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),
);
}
} else {
detail = _networkErrorMessage(error);
}
final localized = _localizeError(
detail,
statusCode,
errorCode: errorCode,
errorParams: errorParams,
);
if (statusCode == 401) {
return UnauthorizedException(message: localized, errorCode: errorCode);
}
if (statusCode == 422) {
return ValidationException(
localized,
errors: data,
statusCode: statusCode,
errorCode: errorCode,
errorParams: errorParams,
);
}
return ServerException(
localized,
statusCode: statusCode,
errorCode: errorCode,
errorParams: errorParams,
);
}
return ServerException(L10n.current.errorNetwork);
}
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;
}
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;
}
if (statusCode == 403) {
return L10n.current.errorForbidden;
}
if (statusCode == 404) {
return L10n.current.errorNotFound;
}
if (statusCode == 429) {
return L10n.current.errorTooManyRequests;
}
if (statusCode != null && statusCode >= 500) {
return L10n.current.errorServer;
}
return L10n.current.errorGenericSafe;
}
static String _networkErrorMessage(DioException error) {
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.sendTimeout ||
error.type == DioExceptionType.receiveTimeout) {
return L10n.current.errorNetworkTimeout;
}
if (error.type == DioExceptionType.connectionError ||
error.type == DioExceptionType.unknown) {
return L10n.current.errorNetworkUnavailable;
}
return L10n.current.errorRequestFailed;
}
}
class ServerException extends ApiException {
const ServerException(
super.message, {
super.statusCode,
super.errorCode,
super.errorParams,
});
}
class UnauthorizedException extends ApiException {
UnauthorizedException({String? message, String? errorCode})
: super(
message ?? L10n.current.errorReLogin,
statusCode: 401,
errorCode: errorCode,
);
}
class ValidationException extends ApiException {
final Map<String, dynamic>? errors;
const ValidationException(
super.message, {
this.errors,
super.statusCode,
super.errorCode,
super.errorParams,
});
}