You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+19Lines changed: 19 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -61,6 +61,25 @@ async def main() -> None:
61
61
user =await client.get("/users/1", response_model=User)
62
62
```
63
63
64
+
### Streaming responses
65
+
66
+
For large responses or server-sent events, stream the body chunk-by-chunk. `stream()` is an async context manager:
67
+
68
+
```python
69
+
from httpware import AsyncClient
70
+
71
+
72
+
asyncdefmain() -> None:
73
+
asyncwith AsyncClient(base_url="https://api.example.com") as client:
74
+
asyncwith client.stream("GET", "/big-file") as response:
75
+
asyncfor chunk in response.aiter_bytes():
76
+
process(chunk)
77
+
```
78
+
79
+
`stream()` auto-raises `StatusError` subclasses on 4xx/5xx with the response body pre-read, so `exc.response.content` is accessible from the caught exception.
80
+
81
+
It does NOT pass through the middleware chain: `Retry`, `Bulkhead`, and any custom middleware are bypassed. (Retry separately refuses to retry any request — stream or non-stream — whose body was an async-iterable, since streams can't replay across attempts.)
82
+
64
83
## Errors
65
84
66
85
All 4xx/5xx responses raise typed exceptions automatically: `NotFoundError`, `ServiceUnavailableError`, `RateLimitedError`, etc. — all subclasses of `httpware.StatusError`. Transport-layer transient failures raise `NetworkError`; the resilience middleware raise `RetryBudgetExhaustedError` and `BulkheadFullError`. Everything inherits `httpware.ClientError`.
Copy file name to clipboardExpand all lines: docs/index.md
+19Lines changed: 19 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -59,6 +59,25 @@ async def main() -> None:
59
59
user =await client.get("/users/1", response_model=User)
60
60
```
61
61
62
+
### Streaming responses
63
+
64
+
For large responses or server-sent events, stream the body chunk-by-chunk. `stream()` is an async context manager:
65
+
66
+
```python
67
+
from httpware import AsyncClient
68
+
69
+
70
+
asyncdefmain() -> None:
71
+
asyncwith AsyncClient(base_url="https://api.example.com") as client:
72
+
asyncwith client.stream("GET", "/big-file") as response:
73
+
asyncfor chunk in response.aiter_bytes():
74
+
process(chunk)
75
+
```
76
+
77
+
`stream()` auto-raises `StatusError` subclasses on 4xx/5xx with the response body pre-read, so `exc.response.content` is accessible from the caught exception.
78
+
79
+
It does NOT pass through the middleware chain: `Retry`, `Bulkhead`, and any custom middleware are bypassed. (Retry separately refuses to retry any request — stream or non-stream — whose body was an async-iterable, since streams can't replay across attempts.)
80
+
62
81
## Errors
63
82
64
83
All 4xx/5xx responses raise typed exceptions automatically: `NotFoundError`, `ServiceUnavailableError`, `RateLimitedError`, etc. — all subclasses of `httpware.StatusError`. Transport-layer transient failures raise `NetworkError`; the resilience middleware raise `RetryBudgetExhaustedError` and `BulkheadFullError`. Everything inherits `httpware.ClientError`.
Copy file name to clipboardExpand all lines: planning/deferred-work.md
+6-5Lines changed: 6 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,10 +4,6 @@ Items raised in reviews that are real but not actionable now.
4
4
5
5
## Open
6
6
7
-
### Retry + streaming bodies (Epic 4 interaction)
8
-
9
-
-**`Retry` re-invokes `next(request)` with the same `httpx2.Request` on each attempt.** Safe for in-memory bytes/JSON bodies; unsafe for streaming/async-iterable bodies (consumed iterator can't replay). When Epic 4 ships `AsyncClient.stream` (`4-3`), Retry needs to refuse to retry streamed-body requests (or document that callers supply a body factory). Spec: `planning/specs/2026-06-05-retry-and-retry-budget-design.md` §"Open questions".
10
-
11
7
### Decoder-side
12
8
13
9
-**`_get_adapter``lru_cache` is module-global, not per-decoder instance** — keyed by `model` only; two `PydanticDecoder()` instances with different configurations (none today) would share adapters, and the cache survives across tests unless explicitly cleared. Revisit if/when a configurable `PydanticDecoder(mode=..., strict=...)` lands. (`src/httpware/decoders/pydantic.py:12-14`)
@@ -19,13 +15,18 @@ PR #21 (`feat/v0.3-pydantic-optional`) shipped 0.3.0 with pydantic moved to `[pr
19
15
-**`pydantic` import not guarded the way `msgspec` is** — closed. `decoders/pydantic.py` now guards via `import_checker.is_pydantic_installed`; `PydanticDecoder.__init__` raises `ImportError` with the install hint; `AsyncClient(decoder=None)` fail-fast in `_default_pydantic_decoder()`.
20
16
-**Empty/malformed payload tests** — closed. `tests/test_decoders_pydantic.py::test_malformed_payload_raises_validation_error` is a 7-case parametrized test pinning current pydantic-core behavior for `b""`, `b"null"`, `b"{}"`, malformed JSON, and invalid UTF-8.
21
17
18
+
## Closed by the 0.5.0 streaming release (2026-06-05)
19
+
20
+
-**`Retry` refuses streamed-body requests.** When `_request_with_body` is called with an async-iterable `content`/`data`/`files`, the request gets `extensions["httpware.streaming_body"] = True`. `Retry.__call__` reads the marker and re-raises with a PEP-678 note on retryable failures instead of retrying with a consumed iterator. Closes the prior Open entry.
21
+
-**`httpx2.StreamError` family escape closed.** The new shared `_httpx2_exception_mapper` catches `httpx2.NetworkError` (which is the parent of `ReadError` / `WriteError` / `CloseError`), so stream-specific exceptions raised during body consumption now map to `httpware.NetworkError` consistently.
22
+
22
23
## Closed by the v0.2 thin-wrapper pivot (2026-06-03)
23
24
24
25
The pivot retired Request/Response/Httpx2Transport/RecordedTransport. The following deferred items are no longer applicable because their host code has been removed or because the responsibility shifted to `httpx2`:
- Unbounded error body size on `StatusError.body` (the `body` field no longer exists; callers reach into `exc.response.content` themselves).
28
-
-`httpx2.StreamError` family escape from the transport's `except httpx2.HTTPError` (mapping logic relocated to AsyncClient's terminal; revisit with Epic 4 streaming work).
29
+
-`httpx2.StreamError` family escape from the transport's `except httpx2.HTTPError` (mapping logic relocated to AsyncClient's terminal; closed by 0.5.0 streaming work — exception mapping in _httpx2_exception_mapper covers the StreamError family via httpx2.NetworkError).
29
30
- Header CRLF / log-injection at the transport seam (host module removed; httpx2 validates).
30
31
- Userinfo on `StatusError.request_url` raw field (the field no longer exists; `__repr__` and summary still sanitize).
31
32
- Concurrent `aclose()` ↔ `__call__` races on `Httpx2Transport` (host class removed; lifecycle is `httpx2`'s concern).
Copy file name to clipboardExpand all lines: planning/engineering.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@ This doc is the single distilled reference for `httpware` design rationale, prot
4
4
5
5
## 1. Project intent
6
6
7
-
`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request` and `httpx2.Response` as the public request/response surface and adds three things on top: typed response decoding (via a `ResponseDecoder` protocol; pydantic and msgspec are both opt-in extras as of 0.3.0), a middleware chain composed at client construction, and a status-keyed exception tree raised automatically on 4xx and 5xx. `AsyncClient(decoder=None)` defaults to constructing a `PydanticDecoder` and so requires the `pydantic` extra; callers can supply an explicit `decoder=` argument to escape the default. As of 0.4.0, the package ships a small resilience suite under `httpware.middleware.resilience` — a `Retry` middleware with a Finagle-style `RetryBudget`, plus a `Bulkhead` concurrency limiter — composed via the standard middleware chain.
7
+
`httpware` is a thin opinionated wrapper around `httpx2`. It re-exports `httpx2.Request` and `httpx2.Response` as the public request/response surface and adds three things on top: typed response decoding (via a `ResponseDecoder` protocol; pydantic and msgspec are both opt-in extras as of 0.3.0), a middleware chain composed at client construction, and a status-keyed exception tree raised automatically on 4xx and 5xx. `AsyncClient(decoder=None)` defaults to constructing a `PydanticDecoder` and so requires the `pydantic` extra; callers can supply an explicit `decoder=` argument to escape the default. As of 0.4.0, the package ships a small resilience suite under `httpware.middleware.resilience` — a `Retry` middleware with a Finagle-style `RetryBudget`, plus a `Bulkhead` concurrency limiter — composed via the standard middleware chain. As of 0.5.0, `AsyncClient.stream()` provides a context-manager API for chunked response bodies; it bypasses the middleware chain by design (see planning/specs/2026-06-05-streaming-design.md).
8
8
9
9
The 0.1.0 release attempted to own a full abstraction over the underlying HTTP client. v0.2 walks that back: `httpx2` is part of the public surface.
10
10
@@ -132,7 +132,7 @@ Post-pivot, the roadmap has three categories. Topic slugs in `planning/specs/` a
132
132
-**Shipped in v0.4 slice 1:**`Retry` middleware + Finagle-style `RetryBudget` token bucket + `attempt_timeout=` parameter (folded-in 3-1). See [`planning/specs/2026-06-05-retry-and-retry-budget-design.md`](specs/2026-06-05-retry-and-retry-budget-design.md) and [`planning/plans/2026-06-05-retry-and-retry-budget-plan.md`](plans/2026-06-05-retry-and-retry-budget-plan.md).
133
133
-**Shipped in v0.4 slice 2:**`Bulkhead` middleware (concurrency limiter via `asyncio.Semaphore` with bounded acquire wait). See [`planning/specs/2026-06-05-bulkhead-design.md`](specs/2026-06-05-bulkhead-design.md) and [`planning/plans/2026-06-05-bulkhead-plan.md`](plans/2026-06-05-bulkhead-plan.md).
134
134
-**Remaining:**`3-6` extension-slot docs.
135
-
-**Epic 4 — Streaming:**`4-3``AsyncClient.stream` context manager (forwards to `httpx2.AsyncClient.stream`; no `StreamResponse` type).
135
+
-**Epic 4 — Streaming:**SHIPPED in v0.5 (PR #…): `AsyncClient.stream()` context manager + Retry refuses streamed-body requests. See [`planning/specs/2026-06-05-streaming-design.md`](specs/2026-06-05-streaming-design.md) and [`planning/plans/2026-06-05-streaming-plan.md`](plans/2026-06-05-streaming-plan.md).
136
136
-**Epic 5 — Observability:**`5-1` Layer 1 middleware hooks, `5-2` wire into resilience middlewares, `5-4` OpenTelemetry middleware (will declare the `otel` extra at the same time the code lands), `5-5` logging policy CI grep.
0 commit comments