httputil depends on xerrors (Tier 0) and valid (Tier 1), placing it at Tier 2. No infrastructure or lifecycle dependencies exist in this module.
2.7 KiB
ADR-002: xerrors.Code to HTTP Status Mapping
Status: Accepted Date: 2026-03-18
Context
HTTP handlers must translate application errors into appropriate HTTP status codes. Without a shared mapping, each handler or controller does its own ad-hoc conversion, leading to inconsistent status codes across endpoints (e.g. one handler returning 500 for a not-found, another returning 404).
xerrors defines stable Code constants aligned with gRPC canonical status names.
The transport layer is responsible for translating those codes to HTTP — this was
an explicit decision in xerrors ADR-001.
Decision
Error(w http.ResponseWriter, err error) is the single entry point for writing
error responses. It uses errors.As to unwrap *xerrors.Err and reads the Code
field. errorCodeToStatus maps each code to an HTTP status:
| xerrors.Code | HTTP Status |
|---|---|
ErrInvalidInput |
400 Bad Request |
ErrUnauthorized |
401 Unauthorized |
ErrPermissionDenied |
403 Forbidden |
ErrNotFound |
404 Not Found |
ErrAlreadyExists |
409 Conflict |
ErrGone |
410 Gone |
ErrPreconditionFailed |
412 Precondition Failed |
ErrRateLimited |
429 Too Many Requests |
ErrInternal |
500 Internal Server Error |
ErrNotImplemented |
501 Not Implemented |
ErrUnavailable |
503 Service Unavailable |
ErrDeadlineExceeded |
504 Gateway Timeout |
| (unknown / nil) | 500 Internal Server Error |
The JSON response body always has the shape {"code": "...", "message": "..."}.
If *xerrors.Err carries context fields (from WithContext), those fields are
merged into the top-level response body alongside code and message.
Errors that do not unwrap to *xerrors.Err (plain errors.New, third-party errors)
are mapped to 500 with a generic "INTERNAL" code and "internal server error"
message. The original error is not exposed to the caller.
Consequences
- All endpoints in a service share the same error shape and status mapping. A
NOT_FOUNDerror always becomes 404, regardless of which handler returns it. - Business logic selects the appropriate
xerrors.Code; the transport layer picks the status code. Neither layer needs to know about the other's mapping. - The
codefield in the JSON response is the stablexerrors.Codestring value (e.g."NOT_FOUND"), not an HTTP status integer. Clients can switch on the code string to distinguish cases within the same status class. - Context fields from
xerrors.Err.WithContextare promoted to top-level JSON fields. This means field names must not collide with"code"or"message". This is a caller responsibility, not enforced by the package. - Unknown or nil errors produce a 500 with no internal detail leaked — defensive by default.