Package
dio
Version
5.11.1
Operating-System
Android, iOS
Adapter
Default Dio
Output of flutter doctor -v
[✓] Flutter (Channel stable, 3.47.4, on macOS 26.6.2 25G83 darwin-arm64, locale en-ID) [2.1s]
• Flutter version 3.47.4 on channel stable at /Users/ryanaidilp/fvm/versions/3.47.0
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 9584c6713b (6 days ago), 2026-09-10 15:25:10 -0700
• Engine revision 06a2e2a110
• Dart version 3.13.3
• DevTools version 2.60.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets,
enable-record-use, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [1,082ms]
• Android SDK at Library/Android/sdk
• Emulator version 37.1.11.0 (build_id 15917651) (CL:N/A)
• Platform android-36, build-tools 36.0.0
• ANDROID_HOME = /Library/Android/sdk
• Java binary at: /Users/ryanaidilp/.sdkman/candidates/java/current/bin/java
This JDK is specified in your Flutter configuration.
To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment Temurin-17.0.4.1+1 (build 17.0.4.1+1)
• All Android licenses accepted.
[!] Xcode - develop for iOS and macOS (Xcode 27.0) [1,277ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 27A266a
! iOS 27.0 Simulator not installed; this may be necessary for iOS and macOS development.
To download and install the platform, open Xcode, select Xcode > Settings > Components,
and click the GET button for the required platform.
For more information, please visit:
https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [7ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (2 available) [6.6s]
• macOS (desktop) • macos • darwin-arm64 • macOS 26.6.2 25G83 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 152.0.7977.84
[✓] Network resources [800ms]
• All expected network resources are available.
Dart Version
3.12.1
Steps to Reproduce
- Register an async interceptor whose
onError/onRequest/onResponse completes the handler (handler.resolve/reject/next), then — after an await — a bug in the same callback (or a racing cancellation) completes the handler a second time. This is a realistic shape for retry-after-reconnect interceptors, which commonly race during flaky connectivity (airplane-mode toggles, cellular↔WiFi handoff) since multiple async completions can overlap.
- Run the following script with
dart run (place under dio/test/):
import 'dart:async';
import 'dart:typed_data';
import 'package:dio/dio.dart';
class RetryAfterAuthInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
final fakeRetryResponse = Response( requestOptions: err.requestOptions,
statusCode: 200,
data: 'retried-ok',
); handler.resolve(fakeRetryResponse);
await Future<void>.delayed(const Duration(milliseconds: 10));
handler.reject(err); // BUG in the interceptor: already resolved above. }}class AlwaysFailAdapter implements HttpClientAdapter { @override
Future<ResponseBody> fetch( RequestOptions options,
Stream<Uint8List>? requestStream, Future<void>? cancelFuture,
) async { throw DioException( requestOptions: options, type: DioExceptionType.badResponse, response: Response(requestOptions: options, statusCode: 401), ); } @override void close({bool force = false}) {}}
Future<void> main() async {
final errors = <Object>[];
final zone = Zone.current.fork(
specification: ZoneSpecification(
handleUncaughtError: (self, parent, zone, error, stack) {
errors.add(error);
print('UNCAUGHT (zone, NOT delivered to caller): $error');
},
),
);
await zone.run(() async {
final dio = Dio()
..interceptors.add(RetryAfterAuthInterceptor())
..httpClientAdapter = AlwaysFailAdapter();
try {
final response = await dio.get<String>('/resource');
print('Call site got a normal response: ${response.data}');
} catch (e) {
print('Caught at call site: $e');
}
await Future<void>.delayed(const Duration(milliseconds: 200));
});
print('zone-level uncaught errors: $errors');
}
We hit the real (non-synthetic) version of this in production, correlating reliably with flaky connectivity — a race between a connection-retry interceptor and cancellation.
Expected Result
_BaseHandler._throwIfCompleted() (lib/src/interceptor.dart) should throw a StateError that identifies which request it belongs to, so it's traceable even when delivered outside the normal DioException channel (e.g. via Zone.handleUncaughtError).
- The dispatch stage in
DioMixin.fetch (lib/src/dio_mixin.dart) should not attempt to complete a handler that a racing cancellation already completed — currently handler.resolve(value, true) / handler.reject(e, true) after _dispatchRequest run unconditionally, unlike the equivalent guarded path in _observeInterceptorCallback.
Actual Result
Call site got a normal response: retried-ok
UNCAUGHT (zone, NOT delivered to caller): Bad state: The `handler` has already been called, make sure each handler gets called only once.
zone-level uncaught errors: [Bad state: The `handler` has already been called, make sure each handler gets called only once.]
The caller's request appears to succeed; the actual failure is a detached, contextless, uncaught error with no way to correlate it to a request or interceptor. In production this surfaces as a bare StateError in crash reporting.
We have a tested fix (PR #2611) threading RequestOptions through _BaseHandler for message context, and adding isCompleted guards to the dispatch stage.
Once you file it, send me the new issue number and I'll update PR #2611's "Fixes #" reference and the commit message to point at it.
Package
dio
Version
5.11.1
Operating-System
Android, iOS
Adapter
Default Dio
Output of
flutter doctor -v[✓] Flutter (Channel stable, 3.47.4, on macOS 26.6.2 25G83 darwin-arm64, locale en-ID) [2.1s] • Flutter version 3.47.4 on channel stable at /Users/ryanaidilp/fvm/versions/3.47.0 • Upstream repository https://github.com/flutter/flutter.git • Framework revision 9584c6713b (6 days ago), 2026-09-10 15:25:10 -0700 • Engine revision 06a2e2a110 • Dart version 3.13.3 • DevTools version 2.60.0 • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-record-use, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration [✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [1,082ms] • Android SDK at Library/Android/sdk • Emulator version 37.1.11.0 (build_id 15917651) (CL:N/A) • Platform android-36, build-tools 36.0.0 • ANDROID_HOME = /Library/Android/sdk • Java binary at: /Users/ryanaidilp/.sdkman/candidates/java/current/bin/java This JDK is specified in your Flutter configuration. To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`. • Java version OpenJDK Runtime Environment Temurin-17.0.4.1+1 (build 17.0.4.1+1) • All Android licenses accepted. [!] Xcode - develop for iOS and macOS (Xcode 27.0) [1,277ms] • Xcode at /Applications/Xcode.app/Contents/Developer • Build 27A266a ! iOS 27.0 Simulator not installed; this may be necessary for iOS and macOS development. To download and install the platform, open Xcode, select Xcode > Settings > Components, and click the GET button for the required platform. For more information, please visit: https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes • CocoaPods version 1.16.2 [✓] Chrome - develop for the web [7ms] • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Connected device (2 available) [6.6s] • macOS (desktop) • macos • darwin-arm64 • macOS 26.6.2 25G83 darwin-arm64 • Chrome (web) • chrome • web-javascript • Google Chrome 152.0.7977.84 [✓] Network resources [800ms] • All expected network resources are available.Dart Version
3.12.1
Steps to Reproduce
onError/onRequest/onResponsecompletes the handler (handler.resolve/reject/next), then — after anawait— a bug in the same callback (or a racing cancellation) completes the handler a second time. This is a realistic shape for retry-after-reconnect interceptors, which commonly race during flaky connectivity (airplane-mode toggles, cellular↔WiFi handoff) since multiple async completions can overlap.dart run(place underdio/test/):We hit the real (non-synthetic) version of this in production, correlating reliably with flaky connectivity — a race between a connection-retry interceptor and cancellation.
Expected Result
_BaseHandler._throwIfCompleted()(lib/src/interceptor.dart) should throw aStateErrorthat identifies which request it belongs to, so it's traceable even when delivered outside the normalDioExceptionchannel (e.g. viaZone.handleUncaughtError).DioMixin.fetch(lib/src/dio_mixin.dart) should not attempt to complete a handler that a racing cancellation already completed — currentlyhandler.resolve(value, true)/handler.reject(e, true)after_dispatchRequestrun unconditionally, unlike the equivalent guarded path in_observeInterceptorCallback.Actual Result
The caller's request appears to succeed; the actual failure is a detached, contextless, uncaught error with no way to correlate it to a request or interceptor. In production this surfaces as a bare
StateErrorin crash reporting.We have a tested fix (PR #2611) threading
RequestOptionsthrough_BaseHandlerfor message context, and addingisCompletedguards to the dispatch stage.Once you file it, send me the new issue number and I'll update PR #2611's "Fixes #" reference and the commit message to point at it.