Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dio/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ See the [Migration Guide][] for the complete breaking changes list.**

## Unreleased

*None.*
- Fix duplicate interceptor handler completions: the dispatch stage no longer double-completes a handler racing a cancellation, and the resulting `StateError` (if any) now identifies the offending request.

## 5.11.1

Expand Down
15 changes: 10 additions & 5 deletions dio/lib/src/dio_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ abstract class DioMixin implements Dio {
return listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() async {
final handler = RequestInterceptorHandler();
final handler = RequestInterceptorHandler(requestOptions);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Use the RequestOptions passed to this callback for the diagnostic context

requestOptions is the outer variable and is only replaced when dispatch starts. If an earlier request interceptor calls handler.next(options.copyWith(path: '/replacement', method: 'POST')), the next interceptor receives those new options, but its handler still captures the original GET /original. If it completes twice, the new error message points to the wrong request. I reproduced this with two request interceptors on the current head. Please construct the handler from state.data as RequestOptions (the same object passed to cb) and add a regression covering replacement of the options between interceptors.

final result = cb(state.data as RequestOptions, handler);
_observeInterceptorCallback(
result,
Expand Down Expand Up @@ -467,7 +467,7 @@ abstract class DioMixin implements Dio {
return listenCancelForAsyncTask(
requestOptions.cancelToken,
Future(() async {
final handler = ResponseInterceptorHandler();
final handler = ResponseInterceptorHandler(requestOptions);
final result = cb(state.data as Response, handler);
_observeInterceptorCallback(
result,
Expand Down Expand Up @@ -495,7 +495,7 @@ abstract class DioMixin implements Dio {
? error
: InterceptorState(assureDioException(error, requestOptions));
Future<InterceptorState> handleError() async {
final handler = ErrorInterceptorHandler();
final handler = ErrorInterceptorHandler(requestOptions);
final result = cb(state.data, handler);
_observeInterceptorCallback(
result,
Expand Down Expand Up @@ -552,9 +552,14 @@ abstract class DioMixin implements Dio {
requestOptions = reqOpt;
try {
final value = await _dispatchRequest<T>(reqOpt);
handler.resolve(value, true);
// A racing cancellation may have already completed this handler.
if (!handler.isCompleted) {
handler.resolve(value, true);
}
} on DioException catch (e) {
handler.reject(e, true);
if (!handler.isCompleted) {
handler.reject(e, true);
}
}
return null;
}),
Expand Down
18 changes: 17 additions & 1 deletion dio/lib/src/interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,29 @@ class InterceptorState<T> {
}

abstract class _BaseHandler {
_BaseHandler([this._requestOptions]);

final _completer = Completer<InterceptorState>();
void Function()? _processNextInQueue;

// Identifies which request a duplicate-completion error belongs to.
final RequestOptions? _requestOptions;

@protected
Future<InterceptorState> get future => _completer.future;

bool get isCompleted => _completer.isCompleted;

void _throwIfCompleted() {
if (_completer.isCompleted) {
final requestOptions = _requestOptions;
final requestDescription = requestOptions == null
? ''
: ' (${requestOptions.method} ${requestOptions.uri})';
throw StateError(
'The `handler` has already been called, '
'make sure each handler gets called only once.',
'make sure each handler gets called only once.'
'$requestDescription',
);
}
}
Expand Down Expand Up @@ -92,6 +102,8 @@ Object? _invokeCallbackDynamically<T, V extends _BaseHandler>(

/// The handler for interceptors to handle before the request has been sent.
class RequestInterceptorHandler extends _BaseHandler {
RequestInterceptorHandler([super.requestOptions]);

/// Deliver the [requestOptions] to the next interceptor.
///
/// Typically, the method should be called once interceptors done
Expand Down Expand Up @@ -152,6 +164,8 @@ class RequestInterceptorHandler extends _BaseHandler {

/// The handler for interceptors to handle after respond.
class ResponseInterceptorHandler extends _BaseHandler {
ResponseInterceptorHandler([super.requestOptions]);

/// Deliver the [response] to the next interceptor.
///
/// Typically, the method should be called once interceptors done
Expand Down Expand Up @@ -203,6 +217,8 @@ class ResponseInterceptorHandler extends _BaseHandler {

/// The handler for interceptors to handle error occurred during the request.
class ErrorInterceptorHandler extends _BaseHandler {
ErrorInterceptorHandler([super.requestOptions]);

/// Deliver the [error] to the next interceptor.
///
/// Typically, the method should be called once interceptors done
Expand Down
38 changes: 35 additions & 3 deletions dio/test/interceptor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ void main() {
allOf([
isA<DioException>(),
(DioException e) => e.error is StateError,
(DioException e) => (e.error as StateError).message == message,
(DioException e) =>
(e.error as StateError).message.startsWith(message),
]),
),
);
Expand All @@ -71,7 +72,8 @@ void main() {
allOf([
isA<DioException>(),
(DioException e) => e.error is StateError,
(DioException e) => (e.error as StateError).message == message,
(DioException e) =>
(e.error as StateError).message.startsWith(message),
]),
),
);
Expand All @@ -81,12 +83,42 @@ void main() {
allOf([
isA<DioException>(),
(DioException e) => e.error is StateError,
(DioException e) => (e.error as StateError).message == message,
(DioException e) =>
(e.error as StateError).message.startsWith(message),
]),
),
);
});

test(
'Duplicate handler call StateError identifies the offending request',
() async {
final dio = Dio()
..options.baseUrl = MockAdapter.mockBase
..httpClientAdapter = MockAdapter()
..interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
handler.next(options);
handler.next(options);
},
),
);
await expectLater(
dio.get('/test'),
throwsA(
allOf([
isA<DioException>(),
(DioException e) => e.error is StateError,
(DioException e) =>
(e.error as StateError).message.contains('GET') &&
(e.error as StateError).message.contains('/test'),
]),
),
);
},
);

group('InterceptorState', () {
test('toString()', () {
final data = DioException(requestOptions: RequestOptions());
Expand Down