httputil

package
v0.1.0-develop.2026060... Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 9, 2026 License: MIT Imports: 18 Imported by: 0

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

View Source
const DefaultClientErrorLogSamplingEvery = 100

DefaultClientErrorLogSamplingEvery is the default deterministic sample rate for 4xx logs when no router-level override is configured.

View Source
const DefaultDecodeJSONLimit int64 = 1 << 20

DefaultDecodeJSONLimit is the default maximum JSON request size (1 MiB).

View Source
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

func AppendCorrelationAttrs(ctx context.Context, attrs []any) []any

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

func CancelReason(ctx context.Context) string

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

func DecodeJSON(r *http.Request, dst any, maxBytes int64) error

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

func DecodeJSONStrict(r *http.Request, dst any, maxBytes int64) error

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

func ParseCanonicalUUID(raw string) (string, bool)

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

func ParseUUIDPathParam(w http.ResponseWriter, r *http.Request, name string) (string, bool)

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

func WithCancelReasonSlot(ctx context.Context) context.Context

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

func WithClientErrorLogSampling(ctx context.Context, routeKey string) context.Context

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL