Skip to content

Commit

Permalink
fix: Catch sync/async* exceptions in interceptor's handlers (#2139)
Browse files Browse the repository at this point in the history
Resolves #2138

### New Pull Request Checklist

- [x] I have read the
[Documentation](https://pub.dev/documentation/dio/latest/)
- [x] I have searched for a similar pull request in the
[project](https://github.com/cfug/dio/pulls) and found none
- [x] I have updated this branch with the latest `main` branch to avoid
conflicts (via merge from master or rebase)
- [x] I have added the required tests to prove the fix/feature I'm
adding
- [ ] I have updated the documentation (if necessary)
- [x] I have run the tests without failures
- [x] I have updated the `CHANGELOG.md` in the corresponding package

---------

Signed-off-by: CaiJingLong <[email protected]>
Co-authored-by: Alex Li <[email protected]>
  • Loading branch information
CaiJingLong and AlexV525 authored Mar 15, 2024
1 parent 63a4e89 commit a3e7dc2
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 25 deletions.
1 change: 1 addition & 0 deletions dio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ See the [Migration Guide][] for the complete breaking changes list.**
## Unreleased

- Fix `receiveTimeout` throws exception after the request has been cancelled.
- Catch sync/async exceptions in interceptors' handlers.

## 5.4.1

Expand Down
53 changes: 31 additions & 22 deletions dio/lib/src/dio_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -375,40 +375,45 @@ abstract class DioMixin implements Dio {
// Convert the request interceptor to a functional callback in which
// we can handle the return value of interceptor callback.
FutureOr Function(dynamic) requestInterceptorWrapper(
InterceptorSendCallback interceptor,
InterceptorSendCallback cb,
) {
return (dynamic incomingState) async {
final state = incomingState as InterceptorState;
if (state.type == InterceptorResultType.next) {
return listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() {
final requestHandler = RequestInterceptorHandler();
interceptor(state.data as RequestOptions, requestHandler);
return requestHandler.future;
Future(() async {
final handler = RequestInterceptorHandler();
final callback = cb(state.data as RequestOptions, handler);
if (callback is Future) {
await callback;
}
return handler.future;
}),
);
} else {
return state;
}
return state;
};
}

// Convert the response interceptor to a functional callback in which
// we can handle the return value of interceptor callback.
FutureOr<dynamic> Function(dynamic) responseInterceptorWrapper(
InterceptorSuccessCallback interceptor,
InterceptorSuccessCallback cb,
) {
return (dynamic incomingState) async {
final state = incomingState as InterceptorState;
if (state.type == InterceptorResultType.next ||
state.type == InterceptorResultType.resolveCallFollowing) {
return listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() {
final responseHandler = ResponseInterceptorHandler();
interceptor(state.data as Response, responseHandler);
return responseHandler.future;
Future(() async {
final handler = ResponseInterceptorHandler();
final callback = cb(state.data as Response, handler);
if (callback is Future) {
await callback;
}
return handler.future;
}),
);
} else {
Expand All @@ -420,32 +425,35 @@ abstract class DioMixin implements Dio {
// Convert the error interceptor to a functional callback in which
// we can handle the return value of interceptor callback.
FutureOr<dynamic> Function(Object) errorInterceptorWrapper(
InterceptorErrorCallback interceptor,
InterceptorErrorCallback cb,
) {
return (error) {
final state = error is InterceptorState
? error
: InterceptorState(assureDioException(error, requestOptions));
Future<InterceptorState> handleError() async {
final errorHandler = ErrorInterceptorHandler();
interceptor(state.data, errorHandler);
return errorHandler.future;
final handler = ErrorInterceptorHandler();
final callback = cb(state.data, handler);
if (callback is Future) {
await callback;
}
return handler.future;
}

// The request has already been cancelled,
// there is no need to listen for another cancellation.
if (state.data is DioException &&
state.data.type == DioExceptionType.cancel) {
return handleError();
} else if (state.type == InterceptorResultType.next ||
}
if (state.type == InterceptorResultType.next ||
state.type == InterceptorResultType.rejectCallFollowing) {
return listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(handleError),
);
} else {
throw error;
}
throw error;
};
}

Expand Down Expand Up @@ -495,20 +503,21 @@ abstract class DioMixin implements Dio {
future = future.catchError(errorInterceptorWrapper(fun));
}
// Normalize errors, converts errors to [DioException].
return future.then<Response<T>>((data) {
try {
final data = await future;
return assureResponse<T>(
data is InterceptorState ? data.data : data,
requestOptions,
);
}).catchError((Object e) {
} catch (e) {
final isState = e is InterceptorState;
if (isState) {
if (e.type == InterceptorResultType.resolve) {
return assureResponse<T>(e.data, requestOptions);
}
}
throw assureDioException(isState ? e.data : e, requestOptions);
});
}
}

Future<Response<dynamic>> _dispatchRequest<T>(RequestOptions reqOpt) async {
Expand Down
6 changes: 3 additions & 3 deletions dio/lib/src/interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -207,19 +207,19 @@ class Interceptor {
}

/// The signature of [Interceptor.onRequest].
typedef InterceptorSendCallback = void Function(
typedef InterceptorSendCallback = FutureOr<void> Function(
RequestOptions options,
RequestInterceptorHandler handler,
);

/// The signature of [Interceptor.onResponse].
typedef InterceptorSuccessCallback = void Function(
typedef InterceptorSuccessCallback = FutureOr<void> Function(
Response<dynamic> response,
ResponseInterceptorHandler handler,
);

/// The signature of [Interceptor.onError].
typedef InterceptorErrorCallback = void Function(
typedef InterceptorErrorCallback = FutureOr<void> Function(
DioException error,
ErrorInterceptorHandler handler,
);
Expand Down
25 changes: 25 additions & 0 deletions dio/test/interceptor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,31 @@ void main() {
expect(response.data['errCode'], 0);
});

test('Caught exceptions before handler called', () async {
final dio = Dio();
const errorMsg = 'interceptor error';
dio.interceptors.add(
InterceptorsWrapper(
// TODO(EVERYONE): Remove the ignorance once we migrated to a higher version of Dart.
// ignore: void_checks
onRequest: (response, handler) {
throw UnsupportedError(errorMsg);
},
),
);
expect(
dio.get('https://www.cloudflare.com'),
throwsA(
isA<DioException>().having(
(dioException) => dioException.error,
'Exception',
isA<UnsupportedError>()
.having((e) => e.message, 'message', errorMsg),
),
),
);
});

group(ImplyContentTypeInterceptor, () {
Dio createDio() {
final dio = Dio();
Expand Down

0 comments on commit a3e7dc2

Please sign in to comment.