Skip to content

Commit 56f27fb

Browse files
committed
docs: rewrite decoder narrative for multi-decoder routing
1 parent 485073d commit 56f27fb

3 files changed

Lines changed: 49 additions & 7 deletions

File tree

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515

1616
```bash
1717
pip install httpware # core only — no decoder
18-
pip install httpware[pydantic] # + PydanticDecoder (the default-decoder path)
19-
pip install httpware[msgspec] # + MsgspecDecoder
18+
pip install httpware[pydantic] # + PydanticDecoder — handles BaseModel + dataclasses + primitives + generics
19+
pip install httpware[msgspec] # + MsgspecDecoder — handles Struct + dataclasses + primitives + generics
20+
pip install httpware[pydantic,msgspec] # both extras — both decoders register; BaseModel routes to pydantic, Struct to msgspec
2021
pip install httpware[all] # everything declared above (pydantic, msgspec, otel)
2122
```
2223

23-
`AsyncClient()` with no `decoder=` argument defaults to constructing a `PydanticDecoder`; that path requires the `pydantic` extra and raises `ImportError` at `AsyncClient.__init__` if it is missing.
24+
`AsyncClient()` resolves `decoders=None` against installed extras: pydantic if installed (first), msgspec if installed (second), or an empty tuple if neither. `AsyncClient()` never raises on missing extras — failure is deferred to the first `response_model=` call, where `MissingDecoderError` fires *before* the HTTP request if no registered decoder claims the model.
2425

2526
## Quickstart
2627

docs/errors.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ ClientError (catch-all for anything httpware raises)
2727
│ └── ServiceUnavailableError (503)
2828
├── RetryBudgetExhaustedError (a retry was needed but the budget refused)
2929
├── BulkheadFullError (acquire_timeout elapsed before a slot opened)
30-
└── DecodeError (response_model= decoder failed; HTTP call itself succeeded)
30+
├── DecodeError (response_model= decoder failed; HTTP call itself succeeded)
31+
└── MissingDecoderError (no registered decoder claims response_model=; fires before the HTTP call)
3132
```
3233

3334
## Status-to-exception mapping
@@ -155,6 +156,20 @@ except DecodeError as exc:
155156
raise
156157
```
157158

159+
## `MissingDecoderError`
160+
161+
Raised by `send()` / `send_with_response()` / verb methods when `response_model=` is set but no registered decoder claims the model. Carries:
162+
163+
- `model: type` — the `response_model=` value that wasn't claimed.
164+
- `registered_names: tuple[str, ...]` — class names of the registered decoders that all rejected the model. Empty tuple means no decoders were registered.
165+
166+
Corrective action depends on the message hint:
167+
168+
- `no decoders registered. Install pip install httpware[pydantic] or pip install httpware[msgspec], or pass decoders=[...] explicitly.` — install an extra or pass an explicit decoder list.
169+
- `registered decoders (PydanticDecoder + MsgspecDecoder) all rejected it.` — your `response_model` type is exotic enough that neither built-in claims it. Pass a custom `ResponseDecoder` via `decoders=[...]`.
170+
171+
Unlike `DecodeError`, this error fires *before* the HTTP request — no traffic is sent.
172+
158173
## See also
159174

160175
- **[Resilience reference](resilience.md)**`AsyncRetry`, `RetryBudget`, `AsyncBulkhead` parameter tables.

docs/index.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ pip install httpware
1313
Optional extras:
1414

1515
```bash
16-
pip install httpware[pydantic] # PydanticDecoder (the default decoder path)
17-
pip install httpware[msgspec] # MsgspecDecoder
16+
pip install httpware[pydantic] # PydanticDecoder — handles BaseModel + dataclasses + primitives + generics
17+
pip install httpware[msgspec] # MsgspecDecoder — handles Struct + dataclasses + primitives + generics
18+
pip install httpware[pydantic,msgspec] # both extras — both decoders register; BaseModel routes to pydantic, Struct to msgspec
1819
```
1920

2021
## First request
@@ -64,6 +65,31 @@ async def main() -> None:
6465

6566
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`.
6667

68+
### Decoder dispatch
69+
70+
When `response_model=` is set, the client walks `decoders` in order and picks
71+
the first decoder whose `can_decode(model)` returns `True`. Both built-in
72+
decoders claim broadly within their library; the ordering encodes your
73+
preference for shared shapes (`dict`, `list[Foo]`, dataclasses, primitives):
74+
75+
```python
76+
# pydantic-first (the default when both extras are installed):
77+
# - BaseModel -> pydantic
78+
# - Struct -> msgspec
79+
# - dict, list -> pydantic (first in list)
80+
AsyncClient(decoders=[PydanticDecoder(), MsgspecDecoder()])
81+
82+
# msgspec-first — same native routing, but shared shapes go to msgspec:
83+
# - BaseModel -> pydantic
84+
# - Struct -> msgspec
85+
# - dict, list -> msgspec
86+
AsyncClient(decoders=[MsgspecDecoder(), PydanticDecoder()])
87+
```
88+
89+
If no registered decoder claims your `response_model`, the call raises
90+
`MissingDecoderError` *before* the HTTP request — see the
91+
[Errors reference](errors.md#missingdecodererror).
92+
6793
### With resilience middleware
6894

6995
Compose resilience middleware at construction; `AsyncBulkhead` goes outside `AsyncRetry` so one slot covers all retry attempts.
@@ -109,7 +135,7 @@ All errors inherit `httpware.ClientError`. The categories:
109135
- **Status errors** (4xx/5xx responses) — raised automatically, no `raise_for_status()` needed: `NotFoundError`, `RateLimitedError`, `ServiceUnavailableError`, and the rest. All subclass `StatusError`.
110136
- **Transport errors** — connection / network / protocol failures before a response arrived. `NetworkError` (transient) subclasses `TransportError`.
111137
- **Resilience refusals**`RetryBudgetExhaustedError` and `BulkheadFullError`, raised by the resilience middleware.
112-
- **Decode errors**`DecodeError`, raised when `response_model=` decoding fails (HTTP call itself succeeded).
138+
- **Decode errors**`DecodeError`, raised when `response_model=` decoding fails (HTTP call itself succeeded). `MissingDecoderError`, raised when no registered decoder claims the `response_model=` type — fires *before* the HTTP call.
113139

114140
See the [Errors reference](errors.md) for the full tree and catching strategies.
115141

0 commit comments

Comments
 (0)