Skip to content

fix(http2_adapter): handle closed streams during uploads#2571

Merged
AlexV525 merged 1 commit into
mainfrom
fix/2469-http2-closed-stream
Jul 21, 2026
Merged

fix(http2_adapter): handle closed streams during uploads#2571
AlexV525 merged 1 commit into
mainfrom
fix/2469-http2-closed-stream

Conversation

@AlexV525

@AlexV525 AlexV525 commented Jul 20, 2026

Copy link
Copy Markdown
Member

Supersedes #2469.

The original report and reproduction are credited to @vanelizarov. This replacement is rebuilt on the current main branch and addresses the unresolved review feedback and the cancellation hang found while auditing the earlier implementation.

Motivation

When an HTTP/2 peer closes a connection before a streamed request body finishes, http2 closes its outgoing stream controller. The request body subscription can then deliver another chunk to the closed sink, producing an unhandled StateError: Bad state: Cannot add event after closing instead of the transport error that caused the closure.

Changes

  • Stop the request body subscription when the outgoing HTTP/2 sink has already closed.
  • Explicitly settle request-body waiting when cancellation stops the subscription, rather than relying on onDone, which is not called after cancellation.
  • Keep cancellation callbacks weakly referenced so shared, never-cancelled tokens do not retain completed request resources.
  • Assert the exact TransportConnectionException for a peer connection closure and cover cancellation while the source stream remains open.
  • Update the dio_http2_adapter changelog.

Verification

With only the new tests applied to main, the connection-closure test fails with the reported StateError, and the cancellation test times out after two seconds. Both pass with this change, and the cancellation test also confirms that the source stream listener is removed.

dart test for plugins/http2_adapter, melos run format, and melos run analyze complete successfully. An independent adversarial pass also exercised 20 concurrent cancellations, send timeout, a synchronous stream controller, and request-source error propagation.

Hosted verification

The min, stable, and beta workflows all pass with the configured httpbun and proxy services. The stable workflow also passes formatting, analysis, publish dry-run, VM/Chrome/Firefox/Flutter tests, the example APK build, and coverage reporting. Changed-file coverage for http2_adapter.dart increases from 75.69% to 82.42%.

AI assistance

Implementation, tests, and local review were performed with Codex. A separate Codex sub-agent performed the adversarial review. The original diagnosis and reproduction came from @vanelizarov in #2469.

New Pull Request Checklist

  • I have read the Documentation
  • I have read the Agent Contribution Guidelines (required if any part of the change was produced with AI assistance)
  • I have searched for a similar pull request in the project and found none (not applicable - this supersedes fix: Http2Adapter does not fail with StateError when connection is closed #2469)
  • I have updated this branch with the latest main branch to avoid conflicts (via merge from master or rebase)
  • I have added the required tests to prove the fix/feature I'm adding
  • I have updated the documentation (if necessary) (not applicable - no public API or documented behavior changes)
  • I have run the tests without failures
  • I have updated the CHANGELOG.md in the corresponding package

Additional context and info (if any)

The replacement preserves the final outgoing-sink close as a normal await; the observed StateError originates from adding the next body chunk after the transport has closed the sink, not from closing the sink again.

Stop request body subscriptions when the peer closes the HTTP/2 connection, while preserving cancellation deallocation behavior.

Co-authored-by: Ivan Elizarov <elizarov.vanya@gmail.com>

Co-Authored-By: Codex <noreply@openai.com>
@github-actions

Copy link
Copy Markdown
Contributor

Code Coverage Report: Only Changed Files listed

Package Base Coverage New Coverage Difference
plugins/http2_adapter/lib/src/connection_manager_imp.dart 🟢 84.29% 🟢 87.14% 🟢 2.85%
plugins/http2_adapter/lib/src/http2_adapter.dart 🟢 75.69% 🟢 82.42% 🟢 6.73%
Overall Coverage 🟢 85.95% 🟢 86.53% 🟢 0.58%

Minimum allowed coverage is 0%, this run produced 86.53%

@CaiJingLong CaiJingLong left a comment

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.

Review conclusion: Approve

Summary of changes

Stops the request-body subscription when the HTTP/2 outgoing sink has already closed (peer connection closure during upload), and explicitly settles the adapter future on cancellation instead of relying on onDone (which is not called after cancel()). Cancellation callbacks now hold only WeakReferences to the subscription/completer/stream so a shared, never-completed cancel token cannot retain completed request resources.

Review details

Dimension Conclusion Notes
Correctness Pass The StateError from stream.outgoingMessages.add(...) on a closed sink is now caught and the subscription is stopped via stopRequestStream(), letting the response listener's onError surface the real TransportConnectionException. Cancellation now completes requestCompleter explicitly, fixing the hang caused by onDone not firing after cancel(). Idempotency holds: stopRequestStream, the cancelFuture.whenComplete, and the sendTimeout path all guard on requestCompleter.isCompleted and use .ignore() on repeated close/cancel.
Tests Pass Two new tests under group('request stream'). Verified locally: with only the tests applied to main, the closure test fails with StateError: Bad state: Cannot add event after closing and the cancellation test times out at 2s; both pass with this change. The cancellation test also asserts requestController.hasListener is false, confirming the subscription is removed.
Style Pass Matches existing patterns (WeakReference already used for streamWR, .ignore() idiom consistent with the rest of the file). No debug residue, no commented-out code. dart analyze clean, dart format clean.
Risk Pass Internal implementation change only; no public API or behavior change for the non-error path. Sensitive area (transport/cancellation) is touched, but the change narrows failure modes rather than broadening them. SDK lower bound is >=3.0.0; WeakReference has been available since Dart 2.17. Regression risk is low: the non-cancellation, non-closure path is behaviorally identical (listen → add → onDone completes → close).
Documentation Pass plugins/http2_adapter/CHANGELOG.md updated under ## Unreleased with a user-facing bullet. No public API or documented behavior changes, so no README/doc-comment updates needed (correctly marked N/A in the PR checklist).
Repo constraints Pass Branch fix/2469-http2-closed-stream follows category/ticket-id-description. Commit uses Conventional Commits (fix(http2_adapter): ..., lowercase imperative subject) with Co-Authored-By trailers for both human and AI contributors. PR description discloses AI assistance and states verification honestly in prose. Supersedes #2469 per the description.

Confirmed key points

  • Correctness: the reported StateError is eliminated and the underlying transport error propagates; the cancellation hang is fixed by explicitly completing requestCompleter in the cancelFuture.whenComplete callback.
  • Tests: regression tests reproduce both bugs on main and pass with the fix; dart test test/http2_test.dart passes (the only failures in the broader suite are environmental — proxy/httpbun/SSL-pinning services not available locally).
  • Risk: no public API change, no SDK bump, no dependency change; sensitive transport path is made more robust.

Suggestions (non-blocking)

  • The three pre-existing environmental test failures (request with payload via proxy, test_suite_test.dart, SSL pinning untrusted certificate tested and allowed #2) are unrelated to this PR and require external services; no action needed here.

This comment was generated by AI agent omp (model: glm-5-2).

@AlexV525
AlexV525 merged commit 28b4897 into main Jul 21, 2026
5 checks passed
@AlexV525
AlexV525 deleted the fix/2469-http2-closed-stream branch July 21, 2026 01:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants