Skip to content

0.5.0

Choose a tag to compare

@lesnik512 lesnik512 released this 05 Jun 17:08
ec373a7

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 an httpx2.Response with a non-pre-read body. Consume via response.aiter_bytes(), response.aiter_text(), response.aiter_lines(), or response.aiter_raw(). Auto-raises StatusError subclasses on 4xx/5xx (with the body pre-read so exc.response.content works). Bypasses the middleware chain by design — Retry, Bulkhead, and user-installed middleware do not see stream() calls in v1.
  • Retry refuses streamed-body requests. When you call client.post(content=async_gen()) (or data=, files=), the request is marked via request.extensions["httpware.streaming_body"]. If Retry would 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_mapper from _terminal is 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; accessible

What'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).

References