Skip to content

Cancelling a response stream subscription leaves the adapter response subscribed #2618

Description

@kaluli123123

Package

dio

Version

5.11.1; reproduced on main 4684e29d with the repository package.

Operating-System

MacOS (Dart VM regression). The reproduction uses a local fake adapter and does not require an external server.

Adapter

The response-stream handler shared by adapters; reproduced through the public Dio API with a streaming test adapter.

Output of flutter doctor -v

Host environment (identifiers redacted)
[!] Flutter (Channel stable, 3.44.7, on macOS 26.6.2 25G83 darwin-arm64, locale zh-Hans-TW) [515ms]
    • Flutter version 3.44.7 on channel stable at <home>/fvm/versions/3.44.7
    ! Warning: `flutter` on your path resolves to <home>/Documents/sdk/flutter/bin/flutter, which is not inside your current Flutter SDK checkout at <home>/fvm/versions/3.44.7. Consider adding <home>/fvm/versions/3.44.7/bin to the front of your path.
    ! Warning: `dart` on your path resolves to /opt/homebrew/Cellar/dart/3.12.2/libexec/bin/dart, which is not inside your current Flutter SDK checkout at <home>/fvm/versions/3.44.7. Consider adding <home>/fvm/versions/3.44.7/bin to the front of your path.
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 84fc5cbb22 (3 months ago), 2026-07-17 10:20:17 -0700
    • Engine revision 69c8c61792
    • Dart version 3.12.2
    • DevTools version 2.57.0
    • Pub download mirror https://pub.flutter-io.cn
    • Flutter download mirror https://storage.flutter-io.cn
    • Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
    • If those were intentional, you can disregard the above warnings; however it is recommended to use "git" directly to perform update checks and upgrades.

[✓] Android toolchain - develop for Android devices (Android SDK version 36.1.0) [1,616ms]
    • Android SDK at <home>/Library/Android/sdk
    • Emulator version 36.6.11.0 (build_id 15507667) (CL:N/A)
    • Platform android-36.1, build-tools 36.1.0
    • ANDROID_HOME = <home>/Library/Android/sdk
    • ANDROID_SDK_ROOT = <home>/Library/Android/sdk
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
      This is the JDK bundled with the latest Android Studio installation on this machine.
      To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
    • Java version OpenJDK Runtime Environment (build 21.0.10+-117844308-b1163.108)
    • All Android licenses accepted.

[!] Xcode - develop for iOS and macOS (Xcode 27.0) [931ms]
    • 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.17.0

[✓] Chrome - develop for the web [4ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[!] Proxy Configuration [3ms]
    • HTTP_PROXY is set
    ! NO_PROXY is not set

[✓] Connected devices
    • Device names and identifiers redacted.

[✓] Network resources [1,707ms]
    • All expected network resources are available.

! Doctor found issues in 3 categories.

Dart Version

3.12.2 stable, macos_arm64.

Steps to Reproduce

Consume one event using response.data!.stream.first, or cancel its subscription, without cancelling the request's optional CancelToken. The consumer is finished but the wrapper keeps its subscription to the adapter response.

import 'dart:async';
import 'dart:typed_data';
import 'package:dio/dio.dart';

class StreamingAdapter implements HttpClientAdapter {
  StreamingAdapter(this.source);
  final StreamController<Uint8List> source;
  @override
  Future<ResponseBody> fetch(RequestOptions options,
      Stream<Uint8List>? requestStream, Future<void>? cancelFuture) async =>
      ResponseBody(source.stream, 200);
  @override
  void close({bool force = false}) {}
}

Future<void> main() async {
  var cancelCount = 0;
  final source = StreamController<Uint8List>(onCancel: () => cancelCount++);
  final dio = Dio()..httpClientAdapter = StreamingAdapter(source);
  final response = await dio.get<ResponseBody>(
    'https://example.test/stream',
    options: Options(responseType: ResponseType.stream),
  );
  final first = response.data!.stream.first;
  source.add(Uint8List.fromList([1]));
  print(await first);
  print('source.hasListener: ${source.hasListener}, cancelCount: $cancelCount');
  // Actual: true, 0. Expected: false, 1.
  unawaited(source.close());
  dio.close();
}

Expected Result

Consumer cancellation should cancel the source response subscription, release adapter resources, stop receive-progress/timeout work, and complete its cancellation future after asynchronous source cleanup finishes.

Actual Result

The source remains subscribed. handleResponseStream propagates pause/resume but has no consumer onCancel cleanup. Normal stream completion, explicit CancelToken cancellation, and timeout handling are separate existing paths.

Four new public-entry regressions fail against unchanged production code: .first, direct cancellation, pause followed by cancellation, and waiting for asynchronous upstream cleanup. The first three observe source.hasListener == true; the last finds that cleanup did not start. This can leave streaming responses active after a page stops consuming them. No claim of measured battery, TCP, or production-memory impact is made.

A focused fix and regression coverage are available in draft PR #2617. The earlier CancelToken fixes and pause/resume work are different cancellation boundaries.

Activity

  1. kaluli123123 commented on Oct 5, 2026

    @kaluli123123
    Author

    The focused fix is available in draft PR #2617, signed head 04771326a0, based on 4684e29dab.

    Consumer cancellation of the public response stream now cancels the adapter-source subscription, stops the receive timer, releases the response body, and waits for asynchronous source cleanup. Normal stream completion and late CancelToken completion remain safe. No public API or adapter signature changes are included.

    Regression coverage exercises .first, direct and paused subscription cancellation, asynchronous cleanup, and late tokens after completion/error. The four original cancellation regressions fail on unchanged production code. The fixed response suite passes 16 tests on VM and 16 on Chrome; full Chrome passes 238 with one skip, Wasm passes 240 with one skip, and native-adapter Flutter tests pass 29. All ten packages passed analysis and formatting. Full VM reports 382 passed, ten skips and five external DNS/network failures reproduced on an independent unchanged baseline.

    Independent read-only runtime review found no blocking issues, including HTTP/2 resource-release and late-cancellation boundaries. The code and regression tests were already validated before this formal report was created; the current publication tree is unchanged. Firefox, minimum-SDK, Android/iOS device integration and upstream CI are not established by these local checks.

    This report was created with the official Bug template through GraphQL, preserving both template labels. Earlier reports #2615 and #2616 were closed and locked by the template-admission workflow; they are historical admission failures, not maintainer rejection of the defect. No new blank-label report or permission override was used.

    The contribution was prepared with Codex assistance. The PR remains a draft pending contributor review, maintainer triage/review, and approval of the fork CI runs. This Issue remains open; no merge or release has been performed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    h: need triageThis issue needs to be categorizeds: bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions