Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Catch sync/async* exceptions in interceptor's handlers #2139

Merged
merged 10 commits into from
Mar 15, 2024
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
Loading