-
Notifications
You must be signed in to change notification settings - Fork 0
Errors on the Wire
Every error rex and its extensions produce is an RFC 9457 problem document,
served as application/problem+json. One format for the whole stack, so a
client parses one shape rather than four.
{
"type": "urn:rex:problem:method-not-allowed",
"title": "Method Not Allowed",
"status": 405,
"detail": "this method is not supported for this path",
"allowed_methods": ["GET", "PUT", "OPTIONS"]
}RFC 9457 obsoletes RFC 7807; the media type and member names are unchanged, so a client written against 7807 reads this without modification.
rextension.WriteProblem(w, r, http.StatusNotFound,
rextension.ProblemNotFound, "no resource exists at this path")Or build it up:
rextension.NewProblem(http.StatusTooManyRequests,
rextension.ProblemRateLimitExceeded, "rate limit exceeded").
WithInstance(requestID).
WithExtra("retry_after_seconds", 30).
Write(w, r)
detailis safe text only. Never assignerr.Error()to it.
An internal error's text routinely carries a table name, a file path, a connection string, or the shape of an internal service — none of which a client needs and all of which help an attacker.
The cause belongs in the log, joined to the response by instance:
id := requestID(r)
app.Logger().WithError(err).WithField("request_id", id).Error("checkout failed")
rextension.NewProblem(500, rextension.ProblemInternal,
"the request could not be completed").
WithInstance(id).
Write(w, r)There is deliberately no separate trace_id member — instance is what the RFC
provides for exactly this, and one identifier in both the response and the log
is what makes the safe-text rule workable rather than merely restrictive.
| Situation | Status | Slug |
|---|---|---|
| no route matches the path | 404 | not-found |
| path exists under other methods | 405 + Allow + allowed_methods
|
method-not-allowed |
Content-Length over the route's cap |
413 + max_bytes
|
payload-too-large |
| a request reached a router before its table was built | 503 | internal |
The 404's detail is deliberately uninformative. "path not under base URL /api"
tells a prober how the application is mounted; the real reason is logged
instead.
OPTIONS with a non-empty Allow set is answered 204 with the header, not as
a problem.
Appended to the type base. Extensions use these, and applications add their own.
unauthorized · forbidden · not-found · method-not-allowed ·
not-acceptable · unsupported-media-type · payload-too-large ·
validation-failed · rate-limit-exceeded · internal ·
dependency-unavailable · bad-request
Each has a constant: rextension.ProblemUnauthorized, and so on.
rextension-validation answers 422 with the errors extension member:
{
"type": "urn:rex:problem:validation-failed",
"title": "Unprocessable Entity",
"status": 422,
"detail": "the request body is invalid",
"errors": [
{"field": "email", "rule": "email", "message": "must be a valid address"},
{"field": "age", "rule": "min", "message": "must be at least 18", "value": "12"}
]
}field is the JSON name, not the Go field name. value is omitted for
anything that could be a credential.
The default type base is a URN — urn:rex:problem: — because a framework has no
domain it can promise will still serve those pages. A URN is stable and commits
the project to no URL that could later 404.
If you do publish documentation, point the base at it once, at startup:
func main() {
rextension.ProblemTypeBase = "https://api.example.com/problems/"
rextension.InstanceBase = "https://api.example.com/requests/"
// …
}Both are package-level and read on every construction, so set them before serving.
Write sets application/problem+json regardless of Accept. Negotiation
applies to success responses: a client that asked for application/xml and then
made a mistake is better served by a machine-readable problem document it did
not ask for than by a 406 carrying no information about what went wrong.
Problem implements MarshalJSONTo and writes the RFC's members in the RFC's
order, then extension members — so a document's shape does not change depending
on whether an extension member happens to be present. Handy for log-reading and
for pinning responses in tests.
rex — Restful Extended eXperience · MIT · © 2026 Kryovyx · pre-1.0 (alpha), interfaces may change
Getting started
Core
Operating
Ecosystem