Documentation
¶
Overview ¶
Package httpmw provides shared HTTP middleware for paper-board services.
Canonical chain order (top-of-stack first), per docs/standards/http-api-conventions.md §2:
- RequestID — read X-Request-Id or mint UUIDv7
- TrustHeaders — Phase 7+ from gateway; pre-Phase-7 acts as a no-op
- OtelHTTP — otelhttp.NewHandler wrapper
- Recover — panic-to-500 with structured log
- Logger — start/end log with route/status/duration
- BodyLimit — http.MaxBytesReader
- AuthStub | Auth — Phase 1: AuthStub stamps DefaultOrgID; Phase 2: real Auth
- (chi.Compress) — gzip (provided by chi)
- Timeout — request-scoped deadline (chi.Middleware.Timeout)
HandleErr is the central HTTP error boundary; every handler converts a service-level error into the canonical envelope via this function.
Index ¶
- Constants
- func AuthStub(orgID uuid.UUID) func(http.Handler) http.Handler
- func BodyLimit(n int64) func(http.Handler) http.Handler
- func HandleErr(w http.ResponseWriter, r *http.Request, svc string, err error)
- func Logger(next http.Handler) http.Handler
- func OtelHTTP(serviceName string) func(http.Handler) http.Handler
- func Recover(svc string) func(http.Handler) http.Handler
- func RequestID(next http.Handler) http.Handler
- func TrustHeaders(next http.Handler) http.Handler
- func WriteError(w http.ResponseWriter, status int, code, message string)
- type ErrorBody
- type ErrorEnvelope
- type ValidationDetail
Constants ¶
const ( HeaderOrgID = "X-Org-Id" HeaderUserID = "X-User-Id" HeaderRoles = "X-Roles" )
Gateway-issued auth header names (Phase 7+).
const HeaderRequestID = "X-Request-Id"
HeaderRequestID is the canonical header name for inbound + outbound request IDs.
Variables ¶
This section is empty.
Functions ¶
func AuthStub ¶
AuthStub stamps a hardcoded org_id into the request ctx. Used in Phase 1.x before identity Phase 2 ships. Phase 2 swaps this for real Auth that validates JWT/API keys and stamps real org_id/user_id/roles.
SECURITY: AuthStub bypasses authentication entirely. Use only behind a trusted boundary (Phase 1.x dev cluster + private network). Phase 2 onward MUST replace it with Auth.
func BodyLimit ¶
BodyLimit caps the inbound request body at n bytes via http.MaxBytesReader. Use 1<<20 (1 MiB) for JSON; raise for endpoints accepting file uploads. Streaming routes (SSE, chunked uploads) should opt out via chi.Router.With.
func HandleErr ¶
HandleErr writes the canonical error envelope and logs the wrapped chain at the HTTP boundary. Code format: "<svc>.<short>" where short comes from errors.Kind (not_found, conflict, ..., internal).
Message is the bare sentinel string (e.g. "not found"). The wrapped detail is NOT echoed to clients — full chain goes only to logs via log.ErrorAttrs.
If err wraps validator.ValidationErrors, individual field failures appear under details with HTTP 400 + invalid_input code.
func Logger ¶
Logger emits one INFO record per request with the canonical observability keys: method, route, status, duration_ms, bytes_in, bytes_out. Place after RequestID + Recover so request_id is in ctx and panics are caught first.
func OtelHTTP ¶
OtelHTTP returns a middleware wrapping next with otelhttp.NewHandler. The service name names the operation; per-route span name refinement happens downstream via chi route patterns.
func Recover ¶
Recover catches panics from downstream handlers, logs structured (panic value + stack + error.kind=panic), and writes the canonical 500 envelope via HandleErr. Service name names the error code suffix.
func RequestID ¶
RequestID reads HeaderRequestID from the inbound request; mints a UUIDv7 if absent. The value is stored in ctx via log.WithRequestID and echoed back on the response header so clients can correlate.
func TrustHeaders ¶
TrustHeaders reads gateway-issued auth headers and writes them into the request ctx. Used Phase 7+ when the gateway terminates auth and downstream services trust headers. Pre-Phase-7, headers are usually empty so this is a silent no-op.
SECURITY: enable only when fronted by an authenticated gateway. Direct internet exposure with TrustHeaders enabled is a confused-deputy risk — any caller can claim any org/user.
func WriteError ¶
func WriteError(w http.ResponseWriter, status int, code, message string)
WriteError emits the canonical envelope without log. For low-level paths where logging happened earlier or is intentionally skipped (e.g. healthz).
Types ¶
type ErrorBody ¶
type ErrorBody struct {
Code string `json:"code"`
Message string `json:"message"`
Details any `json:"details,omitempty"`
}
ErrorBody is the payload inside ErrorEnvelope.
type ErrorEnvelope ¶
type ErrorEnvelope struct {
Error ErrorBody `json:"error"`
}
ErrorEnvelope is the canonical JSON wire shape for HTTP error responses.
type ValidationDetail ¶
type ValidationDetail struct {
Field string `json:"field"`
Rule string `json:"rule"`
Message string `json:"message,omitempty"`
}
ValidationDetail describes a single validator/v10 field failure surfaced in ErrorBody.Details when an HTTP boundary is handed a validator.ValidationErrors.