Documentation
¶
Overview ¶
Package httputil provides shared HTTP response helpers for GoCell handlers, including JSON success/error writers and the standard response envelope.
Error response helpers ¶
Three functions write structured error responses; choose based on where the status code originates:
WriteError(ctx, w, err) — general-purpose fallback. Accepts any error; derives the HTTP status from errcode.Kind via the Kind → Status mapping. Used by generated handlers for un-declared framework 5xx paths (panic recover, infrastructure faults) and for pre-service decode errors.
WriteErrorWithStatus(ctx, w, status, ecErr) — typed-envelope path. The caller (a generated visit{Method}Response method) pins the exact HTTP status drawn from the struct identity (e.g. Get404ErrorResponse → 404). Applies the same 4xx/5xx redaction policy as WriteError; status is never re-derived from ecErr.Kind. Use this only from generated code — business logic must return a typed response struct, not call this directly.
WritePublic(ctx, w, kind, code, message) — framework-internal path for cases where the kind, code, and message are already known at the call site (e.g. middleware writing a fixed auth-failure body). The message argument must be a compile-time const literal (MESSAGE-CONST-LITERAL-01).
For codegen-driven cell adapters the correct pattern is to return a typed response struct (Xxx{Status}ErrorResponse{Body: *errcode.Error}) from the Service method, not to call any WriteError variant directly. See tools/codegen/contractgen/doc.go and docs/architecture/202605061500-adr-typed-response-envelope.md.
Stable Surface ¶
Status writers (write HTTP status + body):
- WriteJSON(w, status, body) — generic typed body writer
- WritePublic(ctx, w, kind, code, message) — typed public error envelope
- WriteError(ctx, w, err) — domain error → wire envelope
- WriteErrorWithStatus(ctx, w, status, ecErr) — explicit status override
- WriteNilResponseInternal(ctx, w) — framework 5xx for nil typed response
- WriteEncodeFaultInternal(ctx, w) — framework 5xx for encode failure
Decorators (do not write status/body):
- AppendCorrelationAttrs(ctx, attrs) — append RequestID/CorrelationID slog.Attr
- WithCancelReasonSlot(ctx) — install writable 499 cancel-reason slot in context
- CancelReason(ctx) — read the 499 cancel-reason label set by the response path
- ParseCanonicalUUID(raw) — parse and normalize UUID to 36-char dashed form
Body decoders (read request, may write 4xx on parse error):
- DecodeJSON(r, dst, maxBytes) / DecodeJSONStrict(r, dst, maxBytes) — JSON body decoder
- ParsePageParams(r) / ParsePageParamsOrWrite(w, r) — pagination query parser
- ParseUUIDPathParam(w, r, name) — UUID path param parser
Logging policy modifiers (no I/O):
- WithClientErrorLogSampling(ctx, routeKey) / WithClientErrorLogSamplingEvery(ctx, routeKey, every)
ref: docs/architecture/202605061500-adr-typed-response-envelope.md
Package httputil provides shared HTTP response helpers for GoCell handlers.
Index ¶
- Constants
- func AppendCorrelationAttrs(ctx context.Context, attrs []any) []any
- func CancelReason(ctx context.Context) string
- func DecodeJSON(r *http.Request, dst any, maxBytes int64) error
- func DecodeJSONStrict(r *http.Request, dst any, maxBytes int64) error
- func ParseCanonicalUUID(raw string) (string, bool)
- func ParsePageParams(r *http.Request) (query.PageParams, error)
- func ParsePageParamsOrWrite(w http.ResponseWriter, r *http.Request) (query.PageParams, bool)
- func ParseUUIDPathParam(w http.ResponseWriter, r *http.Request, name string) (string, bool)
- func WithCancelReasonSlot(ctx context.Context) context.Context
- func WithClientErrorLogSampling(ctx context.Context, routeKey string) context.Context
- func WithClientErrorLogSamplingEvery(ctx context.Context, routeKey string, every int) context.Context
- func WriteEncodeFaultInternal(ctx context.Context, w http.ResponseWriter)
- func WriteError(ctx context.Context, w http.ResponseWriter, err error)
- func WriteErrorWithStatus(ctx context.Context, w http.ResponseWriter, status int, ecErr *errcode.Error)
- func WriteJSON(w http.ResponseWriter, status int, v any)
- func WriteNilResponseInternal(ctx context.Context, w http.ResponseWriter)
- func WritePublic(ctx context.Context, w http.ResponseWriter, kind errcode.Kind, ...)
Constants ¶
const DefaultClientErrorLogSamplingEvery = 100
DefaultClientErrorLogSamplingEvery is the default deterministic sample rate for 4xx logs when no router-level override is configured.
const DefaultDecodeJSONLimit int64 = 1 << 20
DefaultDecodeJSONLimit is the default maximum JSON request size (1 MiB).
const StatusClientClosedRequest = errcode.StatusClientClosedRequest
StatusClientClosedRequest is nginx's non-standard 499 status code returned when the client closes the connection before the server finishes responding.
Variables ¶
This section is empty.
Functions ¶
func AppendCorrelationAttrs ¶
AppendCorrelationAttrs appends request_id / trace_id / span_id slog attributes (when present in ctx) to the supplied slice. Public so generated typed-response handlers and other framework error logging callers can reuse the canonical correlation key set without re-importing ctxkeys directly.
The order matches the framework's existing 4xx/5xx log paths (request_id → trace_id → span_id) so dashboards filtering on column position keep working.
func CancelReason ¶
CancelReason returns the 499 reason recorded for the current request, or the empty string when no slot was installed / no reason was set. Tracing middleware reads this after handler return to stamp the span attribute.
func DecodeJSON ¶
DecodeJSON reads the request body as JSON into dst. The body must contain exactly one JSON value; trailing content is rejected. Unknown fields are silently ignored to maintain backward compatibility.
Errors are returned as *errcode.Error:
- empty body -> ErrValidationFailed, details: [{"key":"reason","value":"empty body"}]
- truncated JSON -> ErrValidationFailed, details: [{"key":"reason","value":"malformed JSON"}]
- syntax error -> ErrValidationFailed, details: [{"key":"reason","value":"malformed JSON"},{"key":"offset","value":N}]
- type mismatch -> ErrValidationFailed, details: [{"key":"reason","value":"type mismatch"},{"key":"field","value":"..."}]
- trailing content -> ErrValidationFailed, details: [{"key":"reason","value":"trailing content after JSON value"}]
- body too large -> ErrBodyTooLarge
- other -> ErrInternal (details not exposed)
func DecodeJSONStrict ¶
DecodeJSONStrict is like DecodeJSON but rejects unknown fields. All errors documented on DecodeJSON apply, plus the unknown field error:
- unknown field → ErrValidationFailed, details: [{"key":"reason","value":"unknown field"},{"key":"field","value":"..."}]
When the destination is a struct, any JSON key that does not match a non-ignored exported field causes a 400 error. Map destinations are unaffected — they accept any key regardless.
func ParseCanonicalUUID ¶
ParseCanonicalUUID parses raw as a UUID and returns the canonical lowercase dashed form when raw is in one of the two wire-allowed shapes:
- 36-char dashed: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- 32-char compact: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Both case variants are accepted and normalized. Anything else returns ("", false), including:
- brace-wrapped Microsoft GUIDs ({xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx})
- urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
- whitespace padding (" xxx-xxx-... " happens to match length-38 brace dispatch in google/uuid v1.6 and would otherwise be silently accepted)
- any other length, embedded whitespace, or stray punctuation
google/uuid.Parse dispatches by string length and accepts all four extra forms above. ParseCanonicalUUID is the single chokepoint that holds GoCell to the OpenAPI 3.0 `format: uuid` convention used in contract.yaml pathParams: one shape on the wire, normalized once at the boundary.
ref: ietf rfc 9562 §4 — uuid string representation; brace-wrapped and urn:uuid: forms are explicitly out of scope for the canonical text form.
func ParsePageParams ¶
func ParsePageParams(r *http.Request) (query.PageParams, error)
ParsePageParams extracts pagination parameters from URL query params. Query params: ?limit=N&cursor=TOKEN
Returns ErrPageSizeExceeded if limit > MaxPageSize. Returns ErrValidationFailed if limit is not a valid integer. Returns ErrCursorInvalid if the cursor exceeds query.MaxCursorTokenBytes — rejecting oversize cursors at the parse boundary bounds the work any handler can be forced to do before the codec's own length guard fires. ref: kubernetes apiserver 4 KiB continue-token guidance. Omitted limits are normalized to DefaultPageSize. Explicit zero or negative limits are rejected to match contract queryParam minimum: 1.
func ParsePageParamsOrWrite ¶
func ParsePageParamsOrWrite(w http.ResponseWriter, r *http.Request) (query.PageParams, bool)
ParsePageParamsOrWrite parses pagination query params from r. On error it writes the domain error response using the request's logging policy and returns ok=false. The caller must return immediately when ok is false.
func ParseUUIDPathParam ¶
ParseUUIDPathParam extracts a UUID-typed path parameter from r and writes a 400 ERR_VALIDATION_INVALID_UUID response if the value is missing or malformed. On success it returns the canonical lowercase dashed UUID string and ok=true; on failure it returns ("", false) after the response has been written, and the caller MUST return immediately.
Accepts both canonical dashed (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) and compact 32-char hex forms; both are normalized to canonical lowercase dashed UUID before being returned. Brace-wrapped {xxx}, urn:uuid:xxx, whitespace padding, and any other shape are rejected with 400 — the helper delegates to ParseCanonicalUUID, which is stricter than google/uuid.Parse on purpose (see uuidcanonical.go).
name MUST match the `pathParams.{name}` key declared in contract.yaml; the same name appears verbatim in the 400 response message so clients can identify the offending parameter.
The contract.yaml convention `pathParams.{name}.format: uuid` is the authoritative discriminator: any handler serving such a path must use this helper at the entry point. The CH-05 governance rule enforces that link statically.
func WithCancelReasonSlot ¶
WithCancelReasonSlot returns a new context carrying a writable slot for the 499 client-cancel reason ("canceled" vs "deadline_exceeded"). Tracing middleware MUST install the slot before invoking the handler chain; without it, setCancelReason is a no-op and CancelReason returns the empty string, causing tracing to fall back to the legacy "context_canceled" label.
func WithClientErrorLogSampling ¶
WithClientErrorLogSampling installs deterministic route-keyed sampling for client-error logs produced through WriteError.
func WithClientErrorLogSamplingEvery ¶
func WithClientErrorLogSamplingEvery(ctx context.Context, routeKey string, every int) context.Context
WithClientErrorLogSamplingEvery installs deterministic route-keyed sampling using the supplied sampling rate. Values less than one are treated as one.
func WriteEncodeFaultInternal ¶
func WriteEncodeFaultInternal(ctx context.Context, w http.ResponseWriter)
WriteEncodeFaultInternal writes a 500 Internal Server Error for the typed-envelope visit encode failure path: when a generated visit method returns a non-nil error before WriteHeader (buffer-then-commit pattern), the handler calls this helper to commit a 500 wire response. Same exempt framework 5xx surface as WriteNilResponseInternal.
func WriteError ¶
func WriteError(ctx context.Context, w http.ResponseWriter, err error)
WriteError writes err in the canonical structured error response format.
func WriteErrorWithStatus ¶
func WriteErrorWithStatus(ctx context.Context, w http.ResponseWriter, status int, ecErr *errcode.Error)
WriteErrorWithStatus writes ecErr in the canonical structured error response format at the *given* HTTP status, instead of deriving the status from errcode.Kind. The same 4xx/5xx redaction policy as WriteError applies (5xx replaces code/message with the public sentinel; Details are stripped by Error.MarshalJSON; Internal never serializes).
Used by typed-response-envelope generated handlers (PR-V1-CONTRACT-TYPED-RESPONSE-ENVELOPE) where the status is pinned to the typed response struct identity (e.g. Get404ErrorResponse) rather than reverse-derived from errcode.Kind. CH-04 governance enforces that the contract.yaml-declared status matches the typed struct's status, so any drift between the explicit status and ecErr.Kind.Status() is caught at codegen time, not at runtime.
func WriteJSON ¶
func WriteJSON(w http.ResponseWriter, status int, v any)
WriteJSON writes v as a JSON response with the given HTTP status code.
func WriteNilResponseInternal ¶
func WriteNilResponseInternal(ctx context.Context, w http.ResponseWriter)
WriteNilResponseInternal writes a 500 Internal Server Error for the typed-envelope nil-response fallback path: when Service.Method returns (nil, nil), the generated handler invokes this helper instead of letting the framework recover from a panic. This is the "un-declared framework 5xx" surface called out in ADR 202605061500-adr-typed-response-envelope.md D1; CH-04 governance registers it in httpHelperWritesStatuses with an empty status set so the resulting 500 is intentionally outside contract.yaml's responses[] declaration surface.
func WritePublic ¶
func WritePublic(ctx context.Context, w http.ResponseWriter, kind errcode.Kind, code errcode.Code, message string)
WritePublic writes a structured error response whose message is deliberately selected by the framework and safe to expose.
The message argument MUST be a compile-time const literal — runtime data belongs in errcode.WithDetails (typed slog.Attr). MESSAGE-CONST-LITERAL-01 archtest extends to this helper: any caller passing a runtime variable as message is statically rejected at the call site.
Types ¶
This section is empty.