From 055cd339de1c730519e667b81ff05704ee589f98 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Mon, 11 Mar 2024 11:16:22 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A5=85=20Throws=20precise=20`StateErr?= =?UTF-8?q?or`=20for=20handler's=20duplicated=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dio/lib/src/interceptor.dart | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/dio/lib/src/interceptor.dart b/dio/lib/src/interceptor.dart index bca8981b2..a062ebb28 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, From 77a88325ea72d8c6061684bc88a45aead5afd4cb Mon Sep 17 00:00:00 2001 From: Alex Li Date: Fri, 15 Mar 2024 23:42:04 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E2=9C=85=20Add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dio/test/interceptor_test.dart | 70 +++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) 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/'; From 460e415d75aca468943e07de89ae7318b4b33508 Mon Sep 17 00:00:00 2001 From: Alex Li Date: Fri, 15 Mar 2024 23:42:18 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9D=20CHANGELOG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dio/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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