Skip to content

Commit 8c0db76

Browse files
lesnik512claude
andcommitted
docs(index): extract pagination recipe and restructure Errors blurb
Quick-Start was carrying a ~37-line `send_with_response` walkthrough that is really a use-case recipe — moves to `docs/recipes/link-header-pagination.md` under the existing Recipes nav group, framed around RFC 5988 Link pagination with `send_with_response` as the mechanism. Quick-Start keeps a one-sentence pointer next to the typed-decoding example. The landing-page `## Errors` section was a single dense paragraph cataloging six exception classes across three categories — replaced with a four-bullet scan list (status / transport / resilience / decode) and a link to the full `errors.md` reference. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9c0c6dc commit 8c0db76

3 files changed

Lines changed: 50 additions & 39 deletions

File tree

docs/index.md

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ async def main() -> None:
6262
print(user.name)
6363
```
6464

65+
Need the raw response **and** a decoded body from the same call (e.g., for header-based pagination)? See [Link header pagination](recipes/link-header-pagination.md) — it uses `send_with_response`.
66+
6567
### With resilience middleware
6668

6769
Compose resilience middleware at construction; `AsyncBulkhead` goes outside `AsyncRetry` so one slot covers all retry attempts.
@@ -81,44 +83,6 @@ async def main() -> None:
8183
user = await client.get("/users/1", response_model=User)
8284
```
8385

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-
Assume `process` and `next_link` are caller-defined — pick a Link header parser that fits.
95-
96-
```python
97-
from httpware import AsyncClient
98-
from pydantic import BaseModel
99-
100-
101-
class Tag(BaseModel):
102-
name: str
103-
104-
105-
async def main() -> None:
106-
async with AsyncClient(base_url="https://gitlab.example/api/v4") as client:
107-
url = "/projects/1/repository/tags"
108-
params: dict[str, str] | None = {"per_page": "100", "page": "1"}
109-
while url:
110-
request = client.build_request("GET", url, params=params)
111-
response, tags = await client.send_with_response(request, response_model=list[Tag])
112-
for tag in tags:
113-
process(tag)
114-
url = next_link(response.headers.get("link")) # caller's parser
115-
params = None # next link carries query
116-
```
117-
118-
For body-only with a high-level verb, prefer `client.get(..., response_model=...)`.
119-
For body-only with a custom `Request`, prefer `client.send(request, response_model=...)`.
120-
`send_with_response` is not for streaming responses — use `stream()`.
121-
12286
### Streaming responses
12387

12488
For large responses or server-sent events, stream the body chunk-by-chunk. `stream()` is an async context manager:
@@ -140,7 +104,14 @@ It does NOT pass through the middleware chain: `AsyncRetry`, `AsyncBulkhead`, an
140104

141105
## Errors
142106

143-
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`.
107+
All errors inherit `httpware.ClientError`. The categories:
108+
109+
- **Status errors** (4xx/5xx responses) — raised automatically, no `raise_for_status()` needed: `NotFoundError`, `RateLimitedError`, `ServiceUnavailableError`, and the rest. All subclass `StatusError`.
110+
- **Transport errors** — connection / network / protocol failures before a response arrived. `NetworkError` (transient) subclasses `TransportError`.
111+
- **Resilience refusals**`RetryBudgetExhaustedError` and `BulkheadFullError`, raised by the resilience middleware.
112+
- **Decode errors**`DecodeError`, raised when `response_model=` decoding fails (HTTP call itself succeeded).
113+
114+
See the [Errors reference](errors.md) for the full tree and catching strategies.
144115

145116
## Observability
146117

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Link header pagination
2+
3+
GitLab, GitHub, and other APIs paginate via the [RFC 5988](https://datatracker.ietf.org/doc/html/rfc5988) `Link` response header: each page response carries a `Link: <…>; rel="next"` header pointing to the next page. To walk all pages you need both the decoded body **and** the response headers from the same call — `client.get(..., response_model=...)` returns only the body.
4+
5+
`send_with_response` returns both atomically. It routes the decoded body through the configured `ResponseDecoder`, so decoder failures surface as `DecodeError` — caught by `except httpware.ClientError` like every other failure mode.
6+
7+
## The pagination loop
8+
9+
```python
10+
from httpware import AsyncClient
11+
from pydantic import BaseModel
12+
13+
14+
class Tag(BaseModel):
15+
name: str
16+
17+
18+
async def main() -> None:
19+
async with AsyncClient(base_url="https://gitlab.example/api/v4") as client:
20+
url = "/projects/1/repository/tags"
21+
params: dict[str, str] | None = {"per_page": "100", "page": "1"}
22+
while url:
23+
request = client.build_request("GET", url, params=params)
24+
response, tags = await client.send_with_response(request, response_model=list[Tag])
25+
for tag in tags:
26+
process(tag)
27+
url = next_link(response.headers.get("link")) # caller's parser
28+
params = None # next link carries query
29+
```
30+
31+
`process` and `next_link` are caller-defined. Pick a Link-header parser that fits your project — there are several on PyPI, and the format is small enough to hand-roll.
32+
33+
## When to use which API
34+
35+
- **Body only, high-level verb:** `client.get(..., response_model=...)`
36+
- **Body only, custom `Request`:** `client.send(request, response_model=...)`
37+
- **Body + response metadata:** `client.send_with_response(request, response_model=...)`
38+
39+
`send_with_response` is not for streaming responses — use [`stream()`](../index.md#streaming-responses) for those.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ nav:
1313
- Recipes:
1414
- modern-di: recipes/modern-di.md
1515
- Phase decorator patterns: recipes/phase-decorator-patterns.md
16+
- Link header pagination: recipes/link-header-pagination.md
1617
- Development:
1718
- Contributing: dev/contributing.md
1819

0 commit comments

Comments
 (0)