Skip to content

Commit

Permalink
🥅 Throws precise StateError for handler's duplicated calls (#2130)
Browse files Browse the repository at this point in the history
### 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
- [x] 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
  • Loading branch information
AlexV525 authored Mar 15, 2024
1 parent a3e7dc2 commit b261a7b
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 5 deletions.
1 change: 1 addition & 0 deletions dio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 26 additions & 4 deletions dio/lib/src/interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ abstract class _BaseHandler {
Future<InterceptorState> 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.
Expand All @@ -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>(requestOptions));
_processNextInQueue?.call();
}
Expand All @@ -50,6 +60,7 @@ class RequestInterceptorHandler extends _BaseHandler {
Response response, [
bool callFollowingResponseInterceptor = false,
]) {
_throwIfCompleted();
_completer.complete(
InterceptorState<Response>(
response,
Expand All @@ -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<DioException>(
error,
Expand All @@ -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>(response),
);
Expand All @@ -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>(
response,
Expand All @@ -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<DioException>(
error,
Expand All @@ -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<DioException>(error),
error.stackTrace,
Expand All @@ -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>(
response,
Expand All @@ -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<DioException>(error, InterceptorResultType.reject),
error.stackTrace,
Expand Down
70 changes: 69 additions & 1 deletion dio/test/interceptor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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>(),
(DioException e) => e.error is StateError,
(DioException e) => (e.error as StateError).message == message,
]),
),
);
await expectLater(
duplicateResponseCalls.get('/test'),
throwsA(
allOf([
isA<DioException>(),
(DioException e) => e.error is StateError,
(DioException e) => (e.error as StateError).message == message,
]),
),
);
await expectLater(
duplicateErrorCalls.get('/'),
throwsA(
allOf([
isA<DioException>(),
(DioException e) => e.error is StateError,
(DioException e) => (e.error as StateError).message == message,
]),
),
);
});

group('Request Interceptor', () {
test('interceptor chain', () async {
final dio = Dio();
Expand Down Expand Up @@ -428,7 +496,7 @@ void main() {
});
});

group('response interceptor', () {
group('Response interceptor', () {
Dio dio;
test('Response Interceptor', () async {
const urlNotFound = '/404/';
Expand Down

0 comments on commit b261a7b

Please sign in to comment.