Errors
Every refusal is a problem document, and its type resolves to a page explaining it.
Every refusal is an RFC 9457 problem document, served as
application/problem+json. Negotiate on the media type rather than on the status
line: it is the only signal that tells a problem document apart from a successful
body that happens to carry a status field.
{
"type": "https://lateano.com/api/problems/invalid-query",
"title": "The query parsed but does not describe a question this resource can answer",
"status": 422,
"detail": "…what was wrong with this particular request…"
}
That title is the real one, character for character, which matters for the
reason the next section gives. The detail varies per request and is elided here
rather than invented — a sample string in that slot would be a sentence this API
does not produce.
The type URI resolves
RFC 9457 permits type to be an opaque identifier. This API gives that up
deliberately: every type is a real address that serves a page describing the
problem. Fetch it and you get the status, the title, and what to do about it.
curl -sS https://lateano.com/api/problems/invalid-query
The URI is derived from the catalogue entry rather than written at the place the
error is raised, and the page is generated from the same entry. So a type that
resolves is a property of how the API is built, not of somebody having remembered
to write a page.
Read detail, not title
title is a short summary that does not change between occurrences — it
identifies the kind of failure and is safe to switch on. detail is what
varies: the field that was wrong, the value that was rejected, the header that
was missing. A client that logs only title throws away everything that would
let a person fix the request.
Four failures that look like one
The commonest mistake in an API client is collapsing every 4xx to “bad request”. These are distinct because the caller’s next move is distinct.
| Status | Type | What went wrong | What to change |
|---|---|---|---|
400 |
missing-media-type |
The request carried a body and no Content-Type. |
Add the header and resend. |
415 |
unsupported-media-type |
The Content-Type is one this API does not read. |
Send JSON. |
400 |
malformed-body |
The body did not parse. | Fix the syntax. |
422 |
invalid-query |
The body parsed and meant nothing this API can answer. | Change the question. Resending will not help. |
The difference that matters is the last row: 400 and 415 say resend it
differently, and 422 says stop sending this.
The whole catalogue
Rather than list every type here — where the list would be a second copy that
goes stale, and where the count would be wrong the day one is added — follow the
type of any refusal you actually receive. Each page is generated from the
catalogue that produces the response, so the two cannot disagree.
Anything the API can refuse with has a page. Anything without a page, it cannot refuse with.
Two caveats, both about addresses that are not resources of this API. The
catalogue carries a too-many-requests type that nothing reaches — there is no
rate limit, and Authentication says why. And
/api/cron/feedback-rollup answers 401 with a plain-text body rather than a
problem document; it is a scheduled job that happens to live under /api/, and
it is outside the catalogue, the reference, and the promise at the top of this
page.