Skip to content

[http] Don't throw in RetryClient when when has read the response stream - #2040

Merged
brianquinlan merged 3 commits into
dart-lang:masterfrom
ChandraChaan:fix/retry-when-reads-stream
Nov 4, 2026
Merged

brianquinlan merged 3 commits into
dart-lang:masterfrom
ChandraChaan:fix/retry-when-reads-stream

Conversation

@ChandraChaan

Copy link
Copy Markdown
Contributor

Before retrying, RetryClient.send drains the discarded response with response.stream.listen(...) so the connection isn't left open. The response stream is single-subscription, so if the when callback already read the body (for example to retry on a specific error payload), listen throws Bad state: Stream has already been listened to and the whole request fails instead of being retried.

The drain is now wrapped in try/on StateError. If when already listened, it has consumed or cancelled the stream itself, so there is nothing left to drain. I catch only StateError rather than everything, so unrelated errors still surface. A single-subscription Stream can't be asked whether it already has a listener, so this needs an // ignore: avoid_catching_errors, as package:http2's hpack.dart already does.

Added a test where when reads the body of a 503 to decide to retry; it threw before this change and now gets the 200 from the second attempt. Changelog entry added under 1.7.0-wip.

Fixes #1226


  • I’ve reviewed the contributor guide and applied the relevant portions to this PR.

@brianquinlan

Copy link
Copy Markdown
Collaborator

I think that this only fixes the issue if when returns true. Maybe we want to add some documentation to the constructor of RetryClient after the existing when description like:

/// If [when] reads the response body (via `(response as StreamedResponse).stream`),
/// it is responsible for draining or cancelling the stream. A body can only be
/// read once, so if [when] returns `false`, reading the returned response's body
/// (including through [get], [post], etc.) will throw a [StateError].

@blaugold - I think that fixing the issue the wrap you wrote it would require buffering the entire stream. Is this PR with the warning about when sufficient?

@ChandraChaan

Copy link
Copy Markdown
Contributor Author

Thanks @brianquinlan, you're right: this only helps when when returns true. If it returns false, the response goes back to the caller with its stream already listened to, so get/post fail with the StateError when they read the body. Fixing that would mean buffering the whole body, which I'd rather not do for a retry helper.

I added your wording to the RetryClient constructor docs in 0cb97d8 and left the retry-path fix as it was. I haven't run the docs build locally, so I'll rely on CI for that.

Comment thread pkgs/http/lib/retry.dart Outdated
/// `(response as StreamedResponse).stream`), it is responsible for draining
/// or cancelling the stream. A body can only be read once, so if [when]
/// returns `false`, reading the returned response's body (including through
/// [get], [post], etc.) will throw a [StateError].

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// [get], [post], etc.) will throw a [StateError].
/// [RetryClient.get], [RetryClient.post], etc.) will throw a [StateError].

@ChandraChaan

Copy link
Copy Markdown
Contributor Author

Thanks, applied your suggestion in b992dc9 ([RetryClient.get] / [RetryClient.post]).

@brianquinlan
brianquinlan merged commit 4646fe6 into dart-lang:master Nov 4, 2026
48 checks passed
@brianquinlan

Copy link
Copy Markdown
Collaborator

@ChandraChaan thank you!

copybara-service Bot pushed a commit to dart-lang/sdk that referenced this pull request Nov 12, 2026
Revisions updated by `dart tools/rev_sdk_deps.dart`.

dartdoc (https://github.com/dart-lang/dartdoc/compare/2f6847b..03ec6df):
  03ec6df6  Thu Nov 5 11:12:45 2026 +0200  Sergey G. Grekhov  Add tests for type parameters (dart-lang/dartdoc#4308)
  14a91302  Tue Nov 3 04:24:32 2026 +0100  Moritz  Migrate to ecosystem health publish check and setup-dart publish workflow (dart-lang/dartdoc#4310)
  cd8bf52c  Mon Nov 2 14:19:25 2026 +0100  Sigurd Meldgaard  Support standalone AOT compilation and embed resources in html_resources.g.dart (dart-lang/dartdoc#4298)
  cb1b1b2f  Mon Nov 2 14:39:28 2026 +0200  Sergey G. Grekhov  Add tests for parameters (dart-lang/dartdoc#4304)
  07506415  Mon Nov 2 12:14:22 2026 +0100  dependabot[bot]  Bump the github-actions group across 1 directory with 3 updates (dart-lang/dartdoc#4311)
  494aec7f  Mon Nov 2 12:14:05 2026 +0100  Sarah Zakarias  Add a "Documentation inherited from" note to inherited doc comments (dart-lang/dartdoc#4309)
  527502a9  Mon Nov 2 11:21:46 2026 +0100  Sarah Zakarias  Give the mock SDK the version of the SDK running the tests (dart-lang/dartdoc#4312)

http (https://github.com/dart-lang/http/compare/1f869d0..4646fe6):
  4646fe6  Thu Nov 5 02:41:04 2026 +0530  Chandra ObulaReddy  [http] Don't throw in RetryClient when `when` has read the response stream (dart-lang/http#2040)

native (https://github.com/dart-lang/native/compare/1838637..6287745):
  6287745ca  Thu Nov 5 20:37:11 2026 +0800  Dz  [hooks_runner] Fix UTF-8 decoding of hook output on Windows (dart-lang/native#3772)
  0eada93f9  Thu Nov 5 19:18:09 2026 +0800  Dz  [native_toolchain_c] Decode vswhere UTF-8 output correctly on Windows (dart-lang/native#3773)

Change-Id: If15b14dac054320ded19f0633a71b77463b9b718
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/574640
Auto-Submit: Nate Bosch <[email protected]>
Commit-Queue: Nicholas Shahan <[email protected]>
Reviewed-by: Nicholas Shahan <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bad state: Stream has already been listened to when accessing response stream in RetryClient.when

2 participants