Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
5 changes: 4 additions & 1 deletion dio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ See the [Migration Guide][] for the complete breaking changes list.**

## Unreleased

*None.*
- Add `DioException.custom(...)` factory and `rejectCustomError(...)` helpers
on interceptor handlers, allowing developers to propagate the original
exception type from inside an `Interceptor` to the caller of the request.
Default behavior is unchanged; opt-in only. Resolves #1950.

## 5.9.2

Expand Down
38 changes: 38 additions & 0 deletions dio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,39 @@ final response = await dio.get('/test');
print(response.data); // 'fake data'
```

##### Throwing custom exceptions from interceptors

By default, `dio` wraps any exception thrown from an interceptor inside a
`DioException`, so callers always see `DioException` at the await boundary.
If you want callers to be able to `catch` your own exception type
directly, opt in via `DioException.custom(...)` or
`handler.rejectCustomError(...)`:

```dart
class UnauthorizedException implements Exception {}

dio.interceptors.add(InterceptorsWrapper(
onResponse: (response, handler) {
if (response.statusCode == 401) {
handler.rejectCustomError(
UnauthorizedException(),
response.requestOptions,
);
return;
}
handler.next(response);
},
));

try {
await dio.get('/protected');
} on UnauthorizedException catch (_) {
// Matches; the DioException wrapper is unwrapped at the pipeline boundary.
}
```

See `DioException.custom` for the full signature.

#### QueuedInterceptor

`Interceptor` can be executed concurrently, that is,
Expand Down Expand Up @@ -631,6 +664,11 @@ StackTrace? stackTrace;

/// The error message that throws a [DioException].
String? message;

/// When `true`, the request pipeline rethrows [error] verbatim at its
/// final boundary instead of throwing this `DioException`. Set by
/// [DioException.custom] / `handler.rejectCustomError`. Defaults to `false`.
bool propagateInnerError;
```

### DioExceptionType
Expand Down
67 changes: 67 additions & 0 deletions dio/lib/src/dio_exception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class DioException implements Exception {
this.error,
StackTrace? stackTrace,
this.message,
this.propagateInnerError = false,
}) : stackTrace = identical(stackTrace, StackTrace.empty)
? requestOptions.sourceStackTrace ?? StackTrace.current
: stackTrace ??
Expand Down Expand Up @@ -183,6 +184,58 @@ class DioException implements Exception {
error: error,
);

/// Wraps a custom [error] and signals the request pipeline to rethrow
/// [error] verbatim (preserving its runtime type) to the caller of the
/// request method, instead of throwing this `DioException` wrapper.
///
/// Use this from inside an [Interceptor] when you want callers to be
/// able to `try { await dio.get(...) } on MyException catch (e)` and
/// match by the original exception type.
///
/// Example:
/// ```dart
/// dio.interceptors.add(InterceptorsWrapper(
/// onResponse: (response, handler) {
/// if (response.statusCode == 401) {
/// handler.reject(DioException.custom(
/// UnauthorizedException(),
/// requestOptions: response.requestOptions,
/// ));
/// return;
/// }
/// handler.next(response);
/// },
/// ));
///
/// try {
/// await dio.get('/protected');
/// } on UnauthorizedException catch (_) {
/// // Matches.
/// }
/// ```
///
/// Internally, the marker survives traversal through subsequent
/// `onError` interceptors and through `QueuedInterceptor`s. The
/// inner [error] is only thrown at the final boundary of `Dio.fetch`.
///
/// Resolves https://github.com/cfug/dio/issues/1950.
factory DioException.custom(
Object error, {
required RequestOptions requestOptions,
StackTrace? stackTrace,
Response? response,
String? message,
}) =>
DioException(
type: DioExceptionType.unknown,
requestOptions: requestOptions,
response: response,
error: error,
stackTrace: stackTrace,
message: message,
propagateInnerError: true,
);

/// The request info for the request that throws exception.
///
/// The info can be empty (e.g. `uri` equals to "")
Expand All @@ -206,6 +259,18 @@ class DioException implements Exception {
/// The error message that throws a [DioException].
final String? message;

/// When `true`, signals that [error] should be rethrown verbatim by
/// the request pipeline at the final boundary of `Dio.fetch`,
/// preserving its runtime type for the caller.
///
/// Set automatically by [DioException.custom] and by the
/// `rejectCustomError(...)` helpers on interceptor handlers. Defaults
/// to `false` for all other constructors and factories — existing
/// behavior is unchanged.
///
/// Resolves https://github.com/cfug/dio/issues/1950.
final bool propagateInnerError;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

isCustom?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — renamed the field (plus the constructor and copyWith params) to isCustom, matching the DioException.custom factory. Tests + README updated.


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

Expand Down
16 changes: 15 additions & 1 deletion dio/lib/src/dio_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,21 @@ 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 was constructed via [DioException.custom] or
// `handler.rejectCustomError(...)`, propagate the inner error
// verbatim so callers can `on MyException catch (e)` directly.
// Resolves https://github.com/cfug/dio/issues/1950.
if (dioException.propagateInnerError && dioException.error != null) {
Error.throwWithStackTrace(
dioException.error!,
dioException.stackTrace,
);
}
throw dioException;
}
}

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

/// Rejects the request with a [DioException.custom] wrapping [error] and
/// signals the pipeline to rethrow [error] verbatim to the caller of the
/// request method, preserving its runtime type.
///
/// Use when you want callers to `on MyException catch (e)` directly,
/// rather than `on DioException catch (e) { final inner = e.error; ... }`.
/// See [DioException.custom] for details.
///
/// Resolves https://github.com/cfug/dio/issues/1950.
void rejectCustomError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would probably go with just rejectCustom everywhere

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done — renamed to rejectCustom on all three handlers, with the doc refs, README, and tests updated to match. 👍

Object error,
RequestOptions requestOptions, [
bool callFollowingErrorInterceptor = false,
]) {
reject(
DioException.custom(error, requestOptions: requestOptions),
callFollowingErrorInterceptor,
);
}
}

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

/// See [RequestInterceptorHandler.rejectCustomError].
void rejectCustomError(
Object error,
RequestOptions requestOptions, [
bool callFollowingErrorInterceptor = false,
]) {
reject(
DioException.custom(error, requestOptions: requestOptions),
callFollowingErrorInterceptor,
);
}
}

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

/// See [RequestInterceptorHandler.rejectCustomError].
void rejectCustomError(
Object error,
RequestOptions requestOptions,
) {
reject(DioException.custom(error, requestOptions: requestOptions));
}
}

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