diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md index 134800694..a68c78ca1 100644 --- a/dio/CHANGELOG.md +++ b/dio/CHANGELOG.md @@ -5,7 +5,7 @@ See the [Migration Guide][] for the complete breaking changes list.** ## Unreleased -*None.* +- Fix request hanging indefinitely when async interceptor callbacks throw without calling the handler. ## 5.9.2 diff --git a/dio/lib/src/dio_mixin.dart b/dio/lib/src/dio_mixin.dart index 7145cf0cd..cd9a79e29 100644 --- a/dio/lib/src/dio_mixin.dart +++ b/dio/lib/src/dio_mixin.dart @@ -386,6 +386,25 @@ abstract class DioMixin implements Dio { } } + // Create an error zone that catches uncaught async errors from + // interceptor callbacks. This prevents requests from hanging when + // an async interceptor throws without calling the handler. + // Synchronous exceptions are not caught here and propagate normally. + Zone createInterceptorZone( + _BaseHandler handler, + void Function(Object error) onUncaughtError, + ) { + return Zone.current.fork( + specification: ZoneSpecification( + handleUncaughtError: (self, parent, zone, error, stackTrace) { + if (!handler.isCompleted) { + onUncaughtError(error); + } + }, + ), + ); + } + // Convert the request interceptor to a functional callback in which // we can handle the return value of interceptor callback. FutureOr Function(dynamic) requestInterceptorWrapper( @@ -398,7 +417,13 @@ abstract class DioMixin implements Dio { requestOptions.cancelToken, Future(() async { final handler = RequestInterceptorHandler(); - cb(state.data as RequestOptions, handler); + createInterceptorZone( + handler, + (error) => handler.reject( + assureDioException(error, requestOptions), + true, + ), + ).run(() => cb(state.data as RequestOptions, handler)); return handler.future; }), ); @@ -420,7 +445,13 @@ abstract class DioMixin implements Dio { requestOptions.cancelToken, Future(() async { final handler = ResponseInterceptorHandler(); - cb(state.data as Response, handler); + createInterceptorZone( + handler, + (error) => handler.reject( + assureDioException(error, requestOptions), + true, + ), + ).run(() => cb(state.data as Response, handler)); return handler.future; }), ); @@ -440,7 +471,10 @@ abstract class DioMixin implements Dio { : InterceptorState(assureDioException(error, requestOptions)); Future handleError() async { final handler = ErrorInterceptorHandler(); - cb(state.data, handler); + createInterceptorZone( + handler, + (error) => handler.next(assureDioException(error, requestOptions)), + ).run(() => cb(state.data, handler)); return handler.future; } diff --git a/dio/test/interceptor_test.dart b/dio/test/interceptor_test.dart index 305c588f3..20ec2dd29 100644 --- a/dio/test/interceptor_test.dart +++ b/dio/test/interceptor_test.dart @@ -426,6 +426,87 @@ void main() { ); }); + test('Async interceptor error does not hang the request', () async { + final dio = Dio() + ..options.baseUrl = MockAdapter.mockBase + ..httpClientAdapter = MockAdapter(); + const errorMsg = 'async interceptor error'; + dio.interceptors.add( + InterceptorsWrapper( + // ignore: void_checks + onRequest: (options, handler) async { + await Future.delayed(const Duration(milliseconds: 10)); + throw UnsupportedError(errorMsg); + }, + ), + ); + await expectLater( + dio.get('/test'), + throwsA( + isA().having( + (e) => e.error, + 'error', + isA() + .having((e) => e.message, 'message', errorMsg), + ), + ), + ); + }); + + test('Async onResponse error does not hang the request', () async { + final dio = Dio() + ..options.baseUrl = MockAdapter.mockBase + ..httpClientAdapter = MockAdapter(); + const errorMsg = 'async response error'; + dio.interceptors.add( + InterceptorsWrapper( + // ignore: void_checks + onResponse: (response, handler) async { + await Future.delayed(const Duration(milliseconds: 10)); + throw UnsupportedError(errorMsg); + }, + ), + ); + await expectLater( + dio.get('/test'), + throwsA( + isA().having( + (e) => e.error, + 'error', + isA() + .having((e) => e.message, 'message', errorMsg), + ), + ), + ); + }); + + test('Async onError interceptor error does not hang', () async { + final dio = Dio() + ..options.baseUrl = MockAdapter.mockBase + ..httpClientAdapter = MockAdapter(); + const errorMsg = 'async error interceptor error'; + dio.interceptors.add( + InterceptorsWrapper( + // ignore: void_checks + onError: (err, handler) async { + await Future.delayed(const Duration(milliseconds: 10)); + throw UnsupportedError(errorMsg); + }, + ), + ); + await expectLater( + dio.get('/test-not-found'), + throwsA( + isA().having( + (e) => e.error, + 'error', + isA() + .having((e) => e.message, 'message', errorMsg), + ), + ), + ); + }); + group(ImplyContentTypeInterceptor, () { Dio createDio() { final dio = Dio();