# 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.