Files
jinzhi/lib/common/network/api_exception.dart

124 lines
3.5 KiB
Dart

import 'package:dio/dio.dart';
import '../../models/pb/errors/errors.pb.dart' as kerrors;
enum ApiExceptionKind { transport, http, backend, decode, cancelled }
/// The single network failure shape exposed to application code.
class ApiException implements Exception {
const ApiException({
required this.kind,
this.httpStatus,
this.backendCode,
this.reason,
this.serverMessage,
this.requestOptions,
this.cause,
});
final ApiExceptionKind kind;
final int? httpStatus;
final int? backendCode;
final String? reason;
final String? serverMessage;
final RequestOptions? requestOptions;
final Object? cause;
bool get isUnauthorized =>
httpStatus == 401 ||
const {'INVALID_TOKEN', 'TOKEN_EXPIRED'}.contains(reason?.toUpperCase());
bool get isRetryable {
if (kind == ApiExceptionKind.transport) return true;
final status = httpStatus;
return status == 408 || status == 429 || (status != null && status >= 500);
}
int? get sessionGeneration =>
requestOptions?.extra[authSessionGenerationExtra] as int?;
static ApiException from(Object error, {StackTrace? stackTrace}) {
if (error is ApiException) return error;
if (error is DioException) {
if (error.error is ApiException) return error.error! as ApiException;
return fromDio(error);
}
if (error is FormatException) {
return ApiException(kind: ApiExceptionKind.decode, cause: error);
}
return ApiException(kind: ApiExceptionKind.decode, cause: error);
}
static ApiException fromDio(DioException error) {
final response = error.response;
final status = response?.statusCode;
final kratos = parseKratosError(response?.data);
if (error.type == DioExceptionType.cancel) {
return ApiException(
kind: ApiExceptionKind.cancelled,
httpStatus: status,
requestOptions: error.requestOptions,
cause: error,
);
}
final kind = kratos != null
? ApiExceptionKind.backend
: status != null
? ApiExceptionKind.http
: ApiExceptionKind.transport;
return ApiException(
kind: kind,
httpStatus: status,
backendCode: kratos?.code,
reason: _normalized(kratos?.reason),
serverMessage: _normalized(kratos?.message),
requestOptions: error.requestOptions,
cause: error,
);
}
@override
String toString() =>
'ApiException($kind, status: $httpStatus, code: $backendCode, '
'reason: $reason, message: $serverMessage)';
}
const authSessionGenerationExtra = 'authSessionGeneration';
kerrors.Status? parseKratosError(Object? raw) {
if (raw is List<int> && raw.isNotEmpty) {
try {
final status = kerrors.Status.fromBuffer(raw);
if (status.code != 0 || status.reason.isNotEmpty) return status;
} catch (_) {
return null;
}
}
if (raw is Map) {
final code = raw['code'];
final reason = '${raw['reason'] ?? ''}';
final message = '${raw['message'] ?? ''}';
if (code is num || reason.isNotEmpty || message.isNotEmpty) {
return kerrors.Status(
code: code is num ? code.toInt() : 0,
reason: reason,
message: message,
);
}
}
return null;
}
String? _normalized(String? value) {
final result = value?.trim() ?? '';
return result.isEmpty ? null : result;
}
class ApiExceptionInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final apiException = ApiException.fromDio(err);
handler.next(err.copyWith(error: apiException));
}
}