A refusal now leaves by one door and is written in the vocabulary of the
address it happened at.
HTTPError loses its struct tags and becomes a plain value; MarshalJSON is the
single place that says what it looks like. Its three members already mapped
onto both standards exactly, so no field was added: Status is `status`, Msg is
`detail`, Code is an extension member (RFC 9457), and the same value reads as
`error` / `error_description` under RFC 6749 §5.2. With() members still merge
at the top level per §3.2, with the document's own written last.
zip.OAuth(r) returns a Router whose routes answer the OAuth vocabulary. The
fact rides the route entry rather than a table of addresses, so it survives
composition, and it is a property of the address rather than of the returned
value — which is what makes it reach the refusals no handler wrote (Bind,
validation, panics). A verb the endpoint does not serve is the router
refusing, and answers a problem document with Allow.
middleware.Recover wrote its own 500 body with c.JSON, a second error surface
that spoke only the default vocabulary; it now returns the error so the one
handler renders it. group() copies oauth beside wrap, so a nested scope keeps
the vocabulary.
Allow on 405 comes from the fiber fork's router and was already correct;
TestAWrongVerbSaysWhichVerbsWork now pins it in both directions so an error
handler edit cannot drop it.
Consumers reading `body.error` read `body.detail` now. Each repo takes the
change when it moves its own pin.
A typed op's only way to refuse is to RETURN an error, and an HTTPError could
carry a status, a code and a sentence. So any route whose non-2xx answer has a
SHAPE could not be a typed op at all — and six subsystems in one consumer are
untyped for exactly that reason, each losing its schema, its prose, its MCP tool
and its CLI command to it:
a prepaid gate answers 402 naming the cap, the spend and the ledger
a plugin build answers 422 carrying the diagnostics that say why it failed
a degraded probe answers 503 carrying the report that says what is degraded
Three separate envelopes were hand-rolled downstream to carry a body BESIDE an
error that could not carry one. That is one missing capability wearing three
costumes, so it is answered once, here.
Detail is an extension in the RFC 9457 sense — the members a problem document
may carry beyond its own. It MERGES rather than nests, so a reader sees one
object instead of a body filed under a key it must know to look in.
THE ENVELOPE IS WRITTEN LAST, and that is the half worth the test rather than
the feature: a domain key called `status` silently overwriting the refusal's own
is how a 402 reads as a 200. Mutation-checked — writing the envelope first lets
a detail key displace it, and the guard says so by name.
ADDITIVE, deliberately. Twelve repositories render errors through this type; a
refusal carrying no detail marshals byte-identically to what they read before,
which is pinned rather than asserted.
ErrPaymentRequired and ErrUnprocessable join the shortcuts because they are the
two statuses that most need a body and had none.
Co-authored-by: Hanzo Dev <[email protected]>