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
`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.
└── MissingDecoderError (no registered decoder claims response_model=; fires before the HTTP call)
31
32
```
32
33
33
34
## Status-to-exception mapping
@@ -155,6 +156,20 @@ except DecodeError as exc:
155
156
raise
156
157
```
157
158
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.
pip install httpware[pydantic,msgspec] # both extras — both decoders register; BaseModel routes to pydantic, Struct to msgspec
18
19
```
19
20
20
21
## First request
@@ -64,6 +65,31 @@ async def main() -> None:
64
65
65
66
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
67
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):
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:
109
135
-**Status errors** (4xx/5xx responses) — raised automatically, no `raise_for_status()` needed: `NotFoundError`, `RateLimitedError`, `ServiceUnavailableError`, and the rest. All subclass `StatusError`.
110
136
-**Transport errors** — connection / network / protocol failures before a response arrived. `NetworkError` (transient) subclasses `TransportError`.
111
137
-**Resilience refusals** — `RetryBudgetExhaustedError` and `BulkheadFullError`, raised by the resilience middleware.
-**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.
113
139
114
140
See the [Errors reference](errors.md) for the full tree and catching strategies.
0 commit comments