124 lines
3.5 KiB
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));
|
|
}
|
|
}
|