Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions dio/lib/src/dio_exception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,60 @@ class DioException implements Exception {
this.error,
StackTrace? stackTrace,
this.message,
this.throwInnerErrorOnFinish = false,
}) : stackTrace = identical(stackTrace, StackTrace.empty)
? requestOptions.sourceStackTrace ?? StackTrace.current
: stackTrace ??
requestOptions.sourceStackTrace ??
StackTrace.current;

/// Creates a [DioException] that will throw the inner [error] when it
/// reaches the final catch block in [Dio.fetch].
///
/// This is useful in interceptors when you want to throw a custom exception
/// that callers can catch directly, rather than having to catch [DioException]
/// and extract the inner error.
///
/// Example:
/// ```dart
/// dio.interceptors.add(InterceptorsWrapper(
/// onResponse: (response, handler) {
/// if (response.statusCode == 401) {
/// handler.reject(DioException.customError(
/// requestOptions: response.requestOptions,
/// error: UnauthorizedException('Token expired'),
/// ));
/// return;
/// }
/// handler.next(response);
/// },
/// ));
///
/// // Caller can now catch the custom exception directly:
/// try {
/// await dio.get('/api');
/// } on UnauthorizedException catch (e) {
/// // Now works!
/// }
/// ```
factory DioException.customError({
required RequestOptions requestOptions,
required Object error,
Response? response,
DioExceptionType type = DioExceptionType.unknown,
StackTrace? stackTrace,
String? message,
}) =>
DioException(
requestOptions: requestOptions,
error: error,
response: response,
type: type,
stackTrace: stackTrace,
message: message,
throwInnerErrorOnFinish: true,
);

factory DioException.badResponse({
required int statusCode,
required RequestOptions requestOptions,
Expand Down Expand Up @@ -206,6 +254,15 @@ class DioException implements Exception {
/// The error message that throws a [DioException].
final String? message;

/// When true, the inner [error] will be thrown instead of this [DioException]
/// when it reaches the final catch block in [Dio.fetch].
///
/// This allows interceptors to throw custom exceptions that callers can
/// catch directly without having to unwrap the [DioException].
///
/// See [DioException.customError] for convenient creation.
final bool throwInnerErrorOnFinish;

/// Users can customize the content of [toString] when thrown.
static DioExceptionReadableStringBuilder readableStringBuilder =
defaultDioExceptionReadableStringBuilder;
Expand All @@ -222,6 +279,7 @@ class DioException implements Exception {
Object? error,
StackTrace? stackTrace,
String? message,
bool? throwInnerErrorOnFinish,
}) {
return DioException(
requestOptions: requestOptions ?? this.requestOptions,
Expand All @@ -230,6 +288,8 @@ class DioException implements Exception {
error: error ?? this.error,
stackTrace: stackTrace ?? this.stackTrace,
message: message ?? this.message,
throwInnerErrorOnFinish:
throwInnerErrorOnFinish ?? this.throwInnerErrorOnFinish,
);
}

Expand Down
9 changes: 8 additions & 1 deletion dio/lib/src/dio_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,14 @@ abstract class DioMixin implements Dio {
return assureResponse<T>(e.data, requestOptions);
}
}
throw assureDioException(isState ? e.data : e, requestOptions);
final dioException =
assureDioException(isState ? e.data : e, requestOptions);
// If the exception is marked for unwrapping, throw the inner error
// instead of the DioException wrapper.
if (dioException.throwInnerErrorOnFinish && dioException.error != null) {
throw dioException.error!;
Comment on lines +526 to +528
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unwrapping via throw dioException.error!; replaces the original stack trace with the stack trace from this catch block. That makes custom exceptions harder to debug since dioException.stackTrace (captured earlier) is lost. Consider throwing the inner error with the original stack trace (e.g., Error.throwWithStackTrace(dioException.error!, dioException.stackTrace)).

Suggested change
// instead of the DioException wrapper.
if (dioException.throwInnerErrorOnFinish && dioException.error != null) {
throw dioException.error!;
// instead of the DioException wrapper, preserving its original
// stack trace when available.
if (dioException.throwInnerErrorOnFinish && dioException.error != null) {
Error.throwWithStackTrace(
dioException.error!,
dioException.stackTrace ?? StackTrace.current,
);

Copilot uses AI. Check for mistakes.
}
throw dioException;
}
}

Expand Down
131 changes: 131 additions & 0 deletions dio/lib/src/interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,52 @@ class RequestInterceptorHandler extends _BaseHandler {
);
_processNextInQueue?.call();
}

/// Completes the request by rejecting with a custom [error] that will be
/// thrown directly to the caller, bypassing the [DioException] wrapper.
///
/// This is useful when you want callers to catch your custom exception type
/// directly instead of having to catch [DioException] and extract the error.
///
/// The [requestOptions] parameter is required to create the wrapper
/// [DioException]. You can obtain it from the [RequestOptions] parameter
/// passed to the interceptor.
///
/// Example:
/// ```dart
/// dio.interceptors.add(InterceptorsWrapper(
/// onRequest: (options, handler) {
/// if (!isAuthenticated) {
/// handler.rejectCustomError(
/// UnauthorizedException('Not logged in'),
/// options,
/// );
/// return;
/// }
/// handler.next(options);
/// },
/// ));
///
/// // Caller can catch the custom exception directly:
/// try {
/// await dio.get('/api');
/// } on UnauthorizedException catch (e) {
/// // Now works!
/// }
/// ```
void rejectCustomError(
Object error,
RequestOptions requestOptions, [
bool callFollowingErrorInterceptor = false,
]) {
reject(
DioException.customError(
requestOptions: requestOptions,
error: error,
),
callFollowingErrorInterceptor,
);
}
}

/// The handler for interceptors to handle after respond.
Expand Down Expand Up @@ -148,6 +194,51 @@ class ResponseInterceptorHandler extends _BaseHandler {
);
_processNextInQueue?.call();
}

/// Completes the request by rejecting with a custom [error] that will be
/// thrown directly to the caller, bypassing the [DioException] wrapper.
///
/// This is useful when you want callers to catch your custom exception type
/// directly instead of having to catch [DioException] and extract the error.
///
/// The [requestOptions] parameter is required to create the wrapper
/// [DioException]. You can obtain it from `response.requestOptions`.
///
/// Example:
/// ```dart
/// dio.interceptors.add(InterceptorsWrapper(
/// onResponse: (response, handler) {
/// if (response.statusCode == 401) {
/// handler.rejectCustomError(
/// UnauthorizedException('Token expired'),
/// response.requestOptions,
/// );
/// return;
/// }
/// handler.next(response);
/// },
/// ));
///
/// // Caller can catch the custom exception directly:
/// try {
/// await dio.get('/api');
/// } on UnauthorizedException catch (e) {
/// // Now works!
/// }
/// ```
void rejectCustomError(
Object error,
RequestOptions requestOptions, [
bool callFollowingErrorInterceptor = false,
]) {
reject(
DioException.customError(
requestOptions: requestOptions,
error: error,
),
callFollowingErrorInterceptor,
);
}
}

/// The handler for interceptors to handle error occurred during the request.
Expand Down Expand Up @@ -186,6 +277,46 @@ class ErrorInterceptorHandler extends _BaseHandler {
);
_processNextInQueue?.call();
}

/// Completes the request by rejecting with a custom [error] that will be
/// thrown directly to the caller, bypassing the [DioException] wrapper.
///
/// This is useful when you want callers to catch your custom exception type
/// directly instead of having to catch [DioException] and extract the error.
///
/// The [requestOptions] parameter is required to create the wrapper
/// [DioException]. You can obtain it from `error.requestOptions`.
///
/// Example:
/// ```dart
/// dio.interceptors.add(InterceptorsWrapper(
/// onError: (error, handler) {
/// if (error.response?.statusCode == 401) {
/// handler.rejectCustomError(
/// UnauthorizedException('Token expired'),
/// error.requestOptions,
/// );
/// return;
/// }
/// handler.next(error);
/// },
/// ));
///
/// // Caller can catch the custom exception directly:
/// try {
/// await dio.get('/api');
/// } on UnauthorizedException catch (e) {
/// // Now works!
/// }
/// ```
void rejectCustomError(Object error, RequestOptions requestOptions) {
reject(
DioException.customError(
requestOptions: requestOptions,
error: error,
),
);
}
}

/// [Interceptor] helps to deal with [RequestOptions], [Response],
Expand Down
Loading