0.5.0
httpware 0.5.0 — Streaming responses
0.5.0 is additive. No breaking changes. Code written against 0.4.0 continues to work unchanged.
This release closes Epic 4 by adding AsyncClient.stream() for chunked response bodies, and closes two longstanding deferred-work items along the way.
New features
AsyncClient.stream(method, url, **kwargs)— async context manager that yields anhttpx2.Responsewith a non-pre-read body. Consume viaresponse.aiter_bytes(),response.aiter_text(),response.aiter_lines(), orresponse.aiter_raw(). Auto-raisesStatusErrorsubclasses on 4xx/5xx (with the body pre-read soexc.response.contentworks). Bypasses the middleware chain by design —Retry,Bulkhead, and user-installed middleware do not seestream()calls in v1.Retryrefuses streamed-body requests. When you callclient.post(content=async_gen())(ordata=,files=), the request is marked viarequest.extensions["httpware.streaming_body"]. IfRetrywould otherwise retry on a failure, it re-raises the original exception with a PEP 678 note instead — preventing the "consumed iterator can't replay" footgun.
Backwards compatibility
Subclassing/extensions preserve every existing catch-block:
- All previously-shipping methods (
get,post, etc.) behave identically. - The internal refactor that extracted
_httpx2_exception_mapperfrom_terminalis byte-for-byte equivalent in dispatch behavior. Tests prove this. - The streaming-body marker (
request.extensions["httpware.streaming_body"]) only affects requests that genuinely have async-iterable bodies. Existing code passing bytes / dict / files-as-bytes is unaffected.
Usage
from httpware import AsyncClient
async def main() -> None:
async with AsyncClient(base_url="https://api.example.com") as client:
async with client.stream("GET", "/big-file") as response:
async for chunk in response.aiter_bytes():
process(chunk)Catch typed status errors on streams the same way as on regular calls:
from httpware import NotFoundError
try:
async with client.stream("GET", "/maybe-missing") as response:
...
except NotFoundError as exc:
body_text = exc.response.text # pre-read; accessibleWhat's still ahead
- Epic 5 (observability hooks + OTel middleware) is unstarted; logging of retry / bulkhead / stream decisions plumbs through then.
- Whether
stream()should compose with the middleware chain is deferred to real-user feedback. Adding it later is purely additive (stream(..., apply_middleware: bool = False)opt-in).