Files
httputil/docs/adr/ADR-002-xerrors-to-http-status-mapping.md
Rene Nochebuena 285293a75b docs(httputil): correct tier from 3 to 2
httputil depends on xerrors (Tier 0) and valid (Tier 1), placing it at
Tier 2. No infrastructure or lifecycle dependencies exist in this module.
2026-03-19 13:09:32 +00:00

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_FOUND error 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 code field in the JSON response is the stable xerrors.Code string 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.WithContext are 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.