|
| 1 | +# httpware 0.8.1 — `DecodeError` closes the decoder-exception gap |
| 2 | + |
| 3 | +**Patch release with one behavior change.** Code that catches `httpware.ClientError` (the advertised catch-all) now actually catches every failure mode of `response_model=` decoding. Code that catches `pydantic.ValidationError` or `msgspec.*` *directly* downstream of `client.send(..., response_model=...)` will no longer match — those exceptions are now wrapped. |
| 4 | + |
| 5 | +## The gap |
| 6 | + |
| 7 | +Before 0.8.1, when `response_model=` was set, `Client.send` and `AsyncClient.send` invoked the active `ResponseDecoder` without a translation step. Whatever the decoder raised — `pydantic.ValidationError` (schema mismatch or malformed JSON via `TypeAdapter.validate_json`), `msgspec.ValidationError`, `msgspec.DecodeError`, or anything else — escaped untranslated. `except httpware.ClientError` did not catch it. Consumers either had to import the decoder library at the call site or skip the decoder entirely and decode the raw `httpx2.Response` by hand. |
| 8 | + |
| 9 | +## The fix |
| 10 | + |
| 11 | +New `httpware.DecodeError(ClientError)` — direct child of `ClientError`, sibling of `StatusError` / `TransportError` / `RetryBudgetExhaustedError` / `BulkheadFullError`. Both `Client.send` and `AsyncClient.send` now wrap the decoder call: |
| 12 | + |
| 13 | +```python |
| 14 | +try: |
| 15 | + return self._decoder.decode(response.content, response_model) |
| 16 | +except Exception as exc: |
| 17 | + raise DecodeError(response=response, model=response_model, original=exc) from exc |
| 18 | +``` |
| 19 | + |
| 20 | +The middleware/`_dispatch` call stays outside the try — transport and status errors are unaffected. Decoder implementers do not need to import or raise `DecodeError`; the seam translates whatever they raise. |
| 21 | + |
| 22 | +Fields on `DecodeError`: |
| 23 | + |
| 24 | +- `response: httpx2.Response` — the response whose body failed to decode (status, headers, request URL all available). |
| 25 | +- `model: type` — the type that was passed as `response_model=`. |
| 26 | +- `original: BaseException` — the underlying library exception. Also available via `__cause__`. |
| 27 | + |
| 28 | +```python |
| 29 | +from httpware import AsyncClient, ClientError, DecodeError |
| 30 | + |
| 31 | + |
| 32 | +try: |
| 33 | + user = await client.get("/users/1", response_model=User) |
| 34 | +except DecodeError as exc: |
| 35 | + _LOGGER.error( |
| 36 | + "decode failed for %s into %s: %s", |
| 37 | + exc.response.request.url, |
| 38 | + exc.model.__name__, |
| 39 | + exc.original, |
| 40 | + ) |
| 41 | + raise |
| 42 | +except ClientError: |
| 43 | + raise |
| 44 | +``` |
| 45 | + |
| 46 | +## Migration |
| 47 | + |
| 48 | +If you catch `pydantic.ValidationError` or `msgspec.*` directly downstream of `client.send(..., response_model=...)`, switch to `except httpware.DecodeError` (or the broader `except httpware.ClientError`). The previously-leaking exceptions weren't a documented contract, so there's no deprecation pass. The fix is the fix. |
| 49 | + |
| 50 | +If you already catch `httpware.ClientError`, nothing changes — your handler now also covers the decode-failure path it should have covered all along. |
| 51 | + |
| 52 | +## Touched surface |
| 53 | + |
| 54 | +- `httpware.DecodeError` — new public class, re-exported from the top level. |
| 55 | +- `Client.send` / `AsyncClient.send` — both wrap the decoder call (one `try/except` each). |
| 56 | +- `ResponseDecoder.decode` — protocol signature unchanged; docstring grows one sentence documenting the seam wrap. |
| 57 | +- `PydanticDecoder` and `MsgspecDecoder` — unchanged. |
| 58 | +- Docs: `docs/errors.md` (hierarchy + new section), `planning/engineering.md` (Seam B contract + §4 paragraph), `README.md` (one-line note on the `response_model=` paragraph). |
| 59 | + |
| 60 | +## See also |
| 61 | + |
| 62 | +- [`planning/specs/2026-06-07-decoder-error-design.md`](../specs/2026-06-07-decoder-error-design.md) — design rationale. |
| 63 | +- [`planning/plans/2026-06-07-decoder-error-plan.md`](../plans/2026-06-07-decoder-error-plan.md) — implementation plan. |
| 64 | +- PR [#32](https://github.com/modern-python/httpware/pull/32). |
0 commit comments