Problems¶
RFC 7807 / 9457 problem responses: a typed
ProblemDetailmodel and aProblemExceptionyou can raise from anywhere.
The model¶
ProblemDetail is a plain pydantic model with the RFC 7807 members: type (a URI
identifying the problem kind, default about:blank), title, status, detail,
and instance. It allows extras (extra='allow'), so you can attach extension
members — an errors list, a trace_id — and they serialize alongside the
standard ones. Being core, it's pydantic-only: constructing one never touches
HTTP; rendering it into a response is the framework glue's job.
Raising a problem¶
Most of the time you don't build a ProblemDetail by hand — you raise a
ProblemException(status, title=..., detail=..., **extensions) and let the glue
render it. The name mirrors the familiar HTTPException: it's a
control-flow signal to emit a response, not a programming error. title defaults
to the HTTP status phrase, so ProblemException(404) is already a valid problem.
Raise it anywhere a request is being handled — a route, or a DI recipe (e.g. an
auth dependency that raises 401 when a token is missing).
from gazebo.problems import ProblemException
def get_plant(plant_id: int) -> dict:
raise ProblemException(
404,
detail=f'no plant with id {plant_id}',
instance=f'/plants/{plant_id}',
)
A catalog of problem types¶
type defaults to about:blank, which says nothing. For the error kinds your
service raises repeatedly, define them once as ProblemTypes — a stable type URI,
a title, a default status — and raise them by reference, supplying only the
per-occurrence detail/instance (and any extension members). A ProblemRegistry
keys them by a short name and hands back the whole set as a catalog, so the type
URIs become linkable: serve registry.catalog() from an endpoint and a client can
resolve a type it received back to its documented meaning.
from gazebo.problems import ProblemRegistry
problems = ProblemRegistry()
# Define each problem kind once: a stable `type` URI, a title, a default status.
PLANT_NOT_FOUND = problems.define(
'plant-not-found',
type='https://errors.example/plant-not-found',
title='Plant not found',
status=404,
)
def get_plant_or_404(plant_id: int) -> dict:
# Raise it by reference; only the per-occurrence detail/instance vary.
raise PLANT_NOT_FOUND.exception(
detail=f'no plant with id {plant_id}',
instance=f'/plants/{plant_id}',
)
# Serve the whole catalog (key -> ProblemType) so a client can resolve a `type` URI.
catalog = problems.catalog()
ProblemType is frozen (a shared constant you reference, never mutate); .problem()
builds a ProblemDetail and .exception() builds the ProblemException to raise. A
catalog endpoint is just an ordinary route returning registry.catalog().
How it becomes a response¶
ProblemDetail is pure pydantic — turning it into an HTTP response is the
framework glue's job. Under GazeboApp / upgrade()
two handlers are registered automatically: one renders any ProblemException you
raise as application/problem+json, and one maps FastAPI's request-validation
failures to a 422 problem so bad input you never wrote a handler for still
comes back as problem+json rather than FastAPI's default {"detail": [...]}
shape. That uniformity is a soft requirement for OGC conformance. See
GazeboApp & upgrade for the
worked example. On non-ASGI frameworks, render ProblemDetail yourself.
Reference¶
See gazebo.problems.