Skip to content

Commit 3c27dd8

Browse files
lesnik512claude
andcommitted
docs: document send_with_response in index + Seam B contract
docs/index.md: new "Response metadata + typed body" subsection with a Link-header pagination example; points body-only callers back at client.get(..., response_model=). planning/engineering.md: Seam B contract now names send_with_response alongside send as the two call sites that wrap decoder exceptions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f285a5c commit 3c27dd8

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

‎docs/index.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,41 @@ async def main() -> None:
8181
user = await client.get("/users/1", response_model=User)
8282
```
8383

84+
### Response metadata + typed body
85+
86+
When you need both the raw `httpx2.Response` (for headers, status, or the
87+
request URL) **and** a typed body, use `send_with_response`. It returns
88+
both atomically and routes the decode through the configured
89+
`ResponseDecoder`, so decoder failures surface as `DecodeError` — caught
90+
by `except httpware.ClientError` like every other failure mode.
91+
92+
Canonical use case: RFC 5988 Link-header pagination.
93+
94+
```python
95+
from httpware import AsyncClient
96+
from pydantic import BaseModel
97+
98+
99+
class Tag(BaseModel):
100+
name: str
101+
102+
103+
async def main() -> None:
104+
async with AsyncClient(base_url="https://gitlab.example/api/v4") as client:
105+
url = "/projects/1/repository/tags"
106+
params: dict[str, str] | None = {"per_page": "100", "page": "1"}
107+
while url:
108+
request = client.build_request("GET", url, params=params)
109+
response, tags = await client.send_with_response(request, response_model=list[Tag])
110+
for tag in tags:
111+
process(tag)
112+
url = next_link(response.headers.get("link")) # caller's parser
113+
params = None # next link carries query
114+
```
115+
116+
For the body-only case, prefer `client.get(..., response_model=...)`.
117+
`send_with_response` is not for streaming responses — use `stream()`.
118+
84119
### Streaming responses
85120

86121
For large responses or server-sent events, stream the body chunk-by-chunk. `stream()` is an async context manager:

‎planning/engineering.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ The 0.1.0 seams numbered 1 (Middleware↔Transport) and 4 (Transport↔httpx2) h
3939
### Seam B: `Client`/`AsyncClient` ↔ `ResponseDecoder`
4040

4141
- **Where:** `src/httpware/client.py` ↔ `src/httpware/decoders/`.
42-
- **Contract:** the decoder is invoked when the caller passes `response_model=`. The protocol is `decode(content: bytes, model: type[T]) -> T`. Any exception raised by `decode` is wrapped by `Client.send` / `AsyncClient.send` into `httpware.DecodeError` (a `ClientError` subclass carrying `response`, `model`, `original`). Decoder implementers do not need to raise `DecodeError` directly.
42+
- **Contract:** the decoder is invoked when the caller passes `response_model=`. The protocol is `decode(content: bytes, model: type[T]) -> T`. Any exception raised by `decode` is wrapped by the call sites in `client.py` — `Client.send` / `AsyncClient.send` (when `response_model=` is set) and `Client.send_with_response` / `AsyncClient.send_with_response` — into `httpware.DecodeError` (a `ClientError` subclass carrying `response`, `model`, `original`). Decoder implementers do not need to raise `DecodeError` directly.
4343
- **Rule:** the decoder must operate on raw bytes in a single parse pass. Two-pass decoding (`json.loads` then `validate_python`) is rejected: a single bytes-in / typed-object-out pass avoids the redundant intermediate `dict` allocation and parses faster. The Pydantic adapter implements this as `TypeAdapter(model).validate_json(content)`, with the `TypeAdapter` itself memoized via `@functools.lru_cache(maxsize=1024)` on a module-level `_get_adapter(model)` factory (the adapter is the expensive part to build). The msgspec adapter implements it as `msgspec.json.decode(content, type=model)`.
4444

4545
### Seam C: `httpware ↔ optional extras`

0 commit comments

Comments
 (0)