diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md index 7c0b42bb6..de1f93155 100644 --- a/dio/CHANGELOG.md +++ b/dio/CHANGELOG.md @@ -7,6 +7,7 @@ See the [Migration Guide][] for the complete breaking changes list.** - Fix `receiveTimeout` throws exception after the request has been cancelled. - Catch sync/async exceptions in interceptors' handlers. +- Throws precise `StateError` for handler's duplicated calls. ## 5.4.1 diff --git a/dio/lib/src/interceptor.dart b/dio/lib/src/interceptor.dart index 5259e8ba5..9ce9f6124 100644 --- a/dio/lib/src/interceptor.dart +++ b/dio/lib/src/interceptor.dart @@ -26,6 +26,15 @@ abstract class _BaseHandler { Future get future => _completer.future; bool get isCompleted => _completer.isCompleted; + + void _throwIfCompleted() { + if (_completer.isCompleted) { + throw StateError( + 'The `handler` has already been called, ' + 'make sure each handler gets called only once.', + ); + } + } } /// The handler for interceptors to handle before the request has been sent. @@ -35,6 +44,7 @@ class RequestInterceptorHandler extends _BaseHandler { /// Typically, the method should be called once interceptors done /// manipulating the [requestOptions]. void next(RequestOptions requestOptions) { + _throwIfCompleted(); _completer.complete(InterceptorState(requestOptions)); _processNextInQueue?.call(); } @@ -50,6 +60,7 @@ class RequestInterceptorHandler extends _BaseHandler { Response response, [ bool callFollowingResponseInterceptor = false, ]) { + _throwIfCompleted(); _completer.complete( InterceptorState( response, @@ -68,8 +79,11 @@ class RequestInterceptorHandler extends _BaseHandler { /// unless [callFollowingErrorInterceptor] is true /// which delivers [InterceptorResultType.rejectCallFollowing] /// to the [InterceptorState]. - void reject(DioException error, - [bool callFollowingErrorInterceptor = false]) { + void reject( + DioException error, [ + bool callFollowingErrorInterceptor = false, + ]) { + _throwIfCompleted(); _completer.completeError( InterceptorState( error, @@ -90,6 +104,7 @@ class ResponseInterceptorHandler extends _BaseHandler { /// Typically, the method should be called once interceptors done /// manipulating the [response]. void next(Response response) { + _throwIfCompleted(); _completer.complete( InterceptorState(response), ); @@ -98,6 +113,7 @@ class ResponseInterceptorHandler extends _BaseHandler { /// Completes the request by resolves the [response] as the result. void resolve(Response response) { + _throwIfCompleted(); _completer.complete( InterceptorState( response, @@ -114,8 +130,11 @@ class ResponseInterceptorHandler extends _BaseHandler { /// unless [callFollowingErrorInterceptor] is true /// which delivers [InterceptorResultType.rejectCallFollowing] /// to the [InterceptorState]. - void reject(DioException error, - [bool callFollowingErrorInterceptor = false]) { + void reject( + DioException error, [ + bool callFollowingErrorInterceptor = false, + ]) { + _throwIfCompleted(); _completer.completeError( InterceptorState( error, @@ -136,6 +155,7 @@ class ErrorInterceptorHandler extends _BaseHandler { /// Typically, the method should be called once interceptors done /// manipulating the [error]. void next(DioException error) { + _throwIfCompleted(); _completer.completeError( InterceptorState(error), error.stackTrace, @@ -145,6 +165,7 @@ class ErrorInterceptorHandler extends _BaseHandler { /// Completes the request by resolves the [response] as the result. void resolve(Response response) { + _throwIfCompleted(); _completer.complete( InterceptorState( response, @@ -156,6 +177,7 @@ class ErrorInterceptorHandler extends _BaseHandler { /// Completes the request by reject with the [error] as the result. void reject(DioException error) { + _throwIfCompleted(); _completer.completeError( InterceptorState(error, InterceptorResultType.reject), error.stackTrace, diff --git a/dio/test/interceptor_test.dart b/dio/test/interceptor_test.dart index 097515ecd..783ff70d2 100644 --- a/dio/test/interceptor_test.dart +++ b/dio/test/interceptor_test.dart @@ -17,6 +17,74 @@ class MyInterceptor extends Interceptor { } void main() { + test('Throws precise StateError for duplicate calls', () async { + const message = 'The `handler` has already been called, ' + 'make sure each handler gets called only once.'; + final duplicateRequestCallsDio = Dio() + ..options.baseUrl = MockAdapter.mockBase + ..httpClientAdapter = MockAdapter() + ..interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) { + handler.next(options); + handler.next(options); + }, + ), + ); + final duplicateResponseCalls = Dio() + ..options.baseUrl = MockAdapter.mockBase + ..httpClientAdapter = MockAdapter() + ..interceptors.add( + InterceptorsWrapper( + onResponse: (response, handler) { + handler.resolve(response); + handler.resolve(response); + }, + ), + ); + final duplicateErrorCalls = Dio() + ..options.baseUrl = MockAdapter.mockBase + ..httpClientAdapter = MockAdapter() + ..interceptors.add( + InterceptorsWrapper( + onError: (error, handler) { + handler.resolve(Response(requestOptions: error.requestOptions)); + handler.resolve(Response(requestOptions: error.requestOptions)); + }, + ), + ); + await expectLater( + duplicateRequestCallsDio.get('/test'), + throwsA( + allOf([ + isA(), + (DioException e) => e.error is StateError, + (DioException e) => (e.error as StateError).message == message, + ]), + ), + ); + await expectLater( + duplicateResponseCalls.get('/test'), + throwsA( + allOf([ + isA(), + (DioException e) => e.error is StateError, + (DioException e) => (e.error as StateError).message == message, + ]), + ), + ); + await expectLater( + duplicateErrorCalls.get('/'), + throwsA( + allOf([ + isA(), + (DioException e) => e.error is StateError, + (DioException e) => (e.error as StateError).message == message, + ]), + ), + ); + }); + group('Request Interceptor', () { test('interceptor chain', () async { final dio = Dio(); @@ -428,7 +496,7 @@ void main() { }); }); - group('response interceptor', () { + group('Response interceptor', () { Dio dio; test('Response Interceptor', () async { const urlNotFound = '/404/';