feat: AsyncClient.stream() + Retry refuses streamed-body requests (0.5.0, Epic 4)#26
Merged
Conversation
…c 4 story 4-3) Single-method addition to AsyncClient. Bypasses the middleware chain (documented rationale: streams don't compose with Retry/Bulkhead/decoder). Maps httpx2 exceptions raised during request + body consumption to httpware exceptions, mirroring _terminal's dispatch ordering. Closes the deferred-work item about httpx2.StreamError-family escape. The Retry-refuses-streamed-body item stays open as a follow-up PR. No auto-raise on 4xx/5xx (matches httpx convention; deliberate divergence from client.get/post/etc. which DO auto-raise — rationale in spec). No StreamResponse wrapper type. No response_model= parameter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per user pushback in brainstorming: - Auto-raise on 4xx/5xx for streams (with body pre-read so exc.response.content works). - Extract shared _httpx2_exception_mapper helper used by _terminal + stream(). - Retry refuses streamed-body requests (closes the deferred-work item). - Keep middleware bypass for v1 (YAGNI; revisit later). Detection mechanism for streamed-body refusal: request.extensions "httpware.streaming_body" marker, set by _request_with_body when content/data/files is an async-iterable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…refuses-streamed-body (0.5.0) 6 TDD tasks on feat/v0.5-streaming. Task 1 is a pure refactor extracting _httpx2_exception_mapper + _raise_on_status_error helpers so _terminal and the new stream() share dispatch logic. Tasks 2-3 wire the streaming-body marker (in _request_with_body) + Retry's refusal-with- PEP-678-note. Task 4 adds AsyncClient.stream() with auto-raise + body pre-read on 4xx/5xx. Task 5 syncs README/docs/index.md/engineering.md/ deferred-work.md + drafts 0.5.0 release notes. Task 6 verifies + pushes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_error Pure refactor of _terminal. Two module-level helpers (one @asynccontextmanager for the httpx2 exception dispatch, one function for the 4xx/5xx StatusError raise). _terminal now reads as: enter the mapper, send, raise on status. Sets up Task 4: AsyncClient.stream() will reuse both helpers verbatim instead of duplicating the dispatch logic. Behavior is byte-for-byte identical to today; the existing terminal tests cover it.
Adds a _is_streaming_body helper and a marker step in _request_with_body: when content / data / files is an async-iterable, set request.extensions['httpware.streaming_body'] = True before sending. Sets up Task 3: Retry will read the marker and refuse to retry streamed-body requests (they can't replay across attempts). Today the marker has no consumer; it's harmless metadata.
Closes the deferred-work item 'Retry + streaming bodies'. When the
request was constructed with an async-iterable content/data/files,
_request_with_body marked request.extensions['httpware.streaming_body']
= True. Retry now reads the marker and re-raises the original failure
with a PEP-678 note ('not retrying — request body is a stream that
cannot replay across attempts') instead of retrying with a consumed
iterator.
Check happens BEFORE budget.try_withdraw() so a refused retry doesn't
consume a budget token.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds AsyncClient.stream(method, url, **kwargs) as a @contextlib.asynccontextmanager method on the client. Mirrors httpx2.AsyncClient.stream() but auto-raises StatusError subclasses on 4xx/5xx (consistent with client.get/post/etc.) with body pre-read so exc.response.content is accessible. Bypasses the middleware chain (v1 design decision — revisit if user feedback warrants). Uses the shared _httpx2_exception_mapper and _raise_on_status_error helpers extracted in the earlier refactor commit, so dispatch logic stays in lockstep with _terminal. Body consumption errors during 'async for chunk in response.aiter_bytes()' propagate through the yield and get mapped to httpware exceptions consistently. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- README + docs/index.md: add 'Streaming responses' subsection - planning/engineering.md §1 + §8: mention stream() in project intent; mark Epic 4 SHIPPED in roadmap - planning/deferred-work.md: close the 'Retry + streaming bodies' open item and update the v0.2-pivot StreamError-escape entry; add a new 'Closed by the 0.5.0 streaming release' section - planning/releases/0.5.0.md: new release notes
…to module constants Final-review feedback: the "httpware.streaming_body" marker key was duplicated at 5 sites (1 write in client.py + 4 reads in retry.py) and the PEP-678 refusal note was duplicated at 4 sites in retry.py. Per project convention (module-level UPPER_CASE constants over inlined string literals; same pattern as _MAX_ATTEMPTS_INVALID, _MAX_CONCURRENT_INVALID, _DEFAULT_DECODER_MISSING_MESSAGE), hoist both: - STREAMING_BODY_MARKER in client.py (public so retry.py can import it; this IS the contract between the two modules) - _STREAMING_BODY_REFUSAL_NOTE in retry.py (private; only used there) retry.py now imports STREAMING_BODY_MARKER from client. No circular import: client doesn't import retry transitively. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes Epic 4 (Streaming). Adds `AsyncClient.stream()` for chunked response bodies and closes two longstanding deferred-work items.
Test Plan
Backwards compatibility
Purely additive:
Refs
Closes Epic 4. Next: 0.5.0 release prep (tag + GH release).
🤖 Generated with Claude Code