Skip to content

0.8.1

Choose a tag to compare

@lesnik512 lesnik512 released this 08 Jun 04:36
a38ace1

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 exc

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.

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 as response_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:
    raise

Migration

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 (one try/except each).
  • ResponseDecoder.decode — protocol signature unchanged; docstring grows one sentence documenting the seam wrap.
  • PydanticDecoder and MsgspecDecoder — unchanged.
  • 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).

See also