-
Notifications
You must be signed in to change notification settings - Fork 0
Response Validation
On by default: a handler's response is checked against the schema declared for the status it wrote.
func (r *GetUser) Responses() map[int]validation.BodySchema {
return map[int]validation.BodySchema{
200: validation.Scalar(UserResponse{}),
404: validation.Scalar(rextension.Problem{}),
}
}Returning nil from Responses() skips it entirely for that route.
A handler that answers with a shape its documentation does not describe. That is worth catching in staging, where it is a test failure, rather than in a client's parser.
validation.NewConfig(validation.WithValidateResponses(false)) // turn it offvalidation.NewConfig(validation.WithStrictResponses(true))With strict responses on, a handler writing a status that is not in
Responses() produces a 500.
Off by default because it is a genuine behaviour change: every status your handlers can produce has to be documented, including the ones from error paths you have not thought about yet. Turned on, it is a strong guarantee — the documentation and the implementation cannot disagree — and it is much easier to adopt in a new service than to retrofit.
501 written by handler, not in Responses() → 500 to the client
The client is told only that something went wrong:
{"type":"urn:rex:problem:internal","title":"Internal Server Error","status":500,
"detail":"the request could not be completed"}The mismatch describes the server's own internals, so it goes to the log:
validation.MiddlewareConfig{Logger: r.Logger()}Without a logger the specifics are simply lost, and the redaction rule becomes unmaintainable in practice. The extension sets it for you.
- Turn on
ValidateResponses(already the default) and run your test suite. - Document every status your handlers actually write, including
4xx.rextension.Problemis the schema for all of them. - Turn on
WithStrictResponses(true)in staging first. - Watch the log for strict-mode rejections before promoting.
A useful shortcut: document the problem responses once and reuse the map.
func problemResponses(codes ...int) map[int]validation.BodySchema {
m := make(map[int]validation.BodySchema, len(codes))
for _, c := range codes {
m[c] = validation.Scalar(rextension.Problem{})
}
return m
}
func (r *GetUser) Responses() map[int]validation.BodySchema {
m := problemResponses(400, 401, 403, 404, 500)
m[200] = validation.Scalar(UserResponse{})
return m
}The response is re-encoded through the negotiated codec, so a route
declaring a UserResponse answers XML to a client that asked for it, without
the handler knowing. See Codecs and Negotiation.
Response validation buffers and re-decodes the response body. That is fine for ordinary JSON payloads and wrong for anything streamed or large.
For a streaming endpoint, return nil from Responses() — the route is then
passed through untouched, which is what you want for server-sent events or a
file download.
rextension-validation — request/response validation and content negotiation for Rex · MIT · © 2026 Kryovyx