0.8.1
httpware 0.8.1 — DecodeError closes the decoder-exception gap
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.
The gap
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.
The fix
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:
try:
return self._decoder.decode(response.content, response_model)
except Exception as exc:
raise DecodeError(response=response, model=response_model, original=exc) from excThe 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.
Fields on DecodeError:
response: httpx2.Response— the response whose body failed to decode (status, headers, request URL all available).model: type— the type that was passed asresponse_model=.original: BaseException— the underlying library exception. Also available via__cause__.
from httpware import AsyncClient, ClientError, DecodeError
try:
user = await client.get("/users/1", response_model=User)
except DecodeError as exc:
_LOGGER.error(
"decode failed for %s into %s: %s",
exc.response.request.url,
exc.model.__name__,
exc.original,
)
raise
except ClientError:
raiseMigration
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.
If you already catch httpware.ClientError, nothing changes — your handler now also covers the decode-failure path it should have covered all along.
Touched surface
httpware.DecodeError— new public class, re-exported from the top level.Client.send/AsyncClient.send— both wrap the decoder call (onetry/excepteach).ResponseDecoder.decode— protocol signature unchanged; docstring grows one sentence documenting the seam wrap.PydanticDecoderandMsgspecDecoder— unchanged.- Docs:
docs/errors.md(hierarchy + new section),planning/engineering.md(Seam B contract + §4 paragraph),README.md(one-line note on theresponse_model=paragraph).
See also
planning/specs/2026-06-07-decoder-error-design.md— design rationale.planning/plans/2026-06-07-decoder-error-plan.md— implementation plan.- PR #32.