Documentation
¶
Overview ¶
Package httpx is wowapi's HTTP toolbox: response envelopes, the RFC 9457 problem-details error writer, strict JSON decoding, metadata-enforced route registration, and the request helpers module handlers compose. The kernel provides helpers; modules keep the request flow visible (blueprint 05 §1, 04 §4–5).
Index ¶
- Constants
- func ActorFrom(ctx context.Context) (authz.Actor, bool)
- func BindAndValidate[T any](r *http.Request, v *validation.Validator, maxBytes int64) (T, error)
- func Chain(h http.Handler, mws ...Middleware) http.Handler
- func DecodeJSON[T any](r *http.Request, maxBytes int64) (T, error)
- func ETagFrom(version int) string
- func KeyByActor(r *http.Request) string
- func KeyByIP(r *http.Request) string
- func ParseFilters(r *http.Request, allow filtering.Allowlist) (filtering.Set, error)
- func ParsePagination(r *http.Request, def pagination.Defaults) (pagination.Request, error)
- func ParseResourceID(r *http.Request, param string) (uuid.UUID, error)
- func ParseSort(r *http.Request, allow filtering.SortAllowlist) (filtering.Sort, error)
- func RequestHash(r *http.Request, canonical []byte) string
- func RequestIDFrom(ctx context.Context) string
- func RequireIfMatch(r *http.Request) (int, error)
- func WithActor(ctx context.Context, a authz.Actor) context.Context
- func WithIdempotency(ctx context.Context, w http.ResponseWriter, r *http.Request, ...)
- func WithRequestID(ctx context.Context, id string) context.Context
- func WriteError(ctx context.Context, w http.ResponseWriter, err error)
- func WriteJSON[T any](w http.ResponseWriter, status int, body T)
- type APIResponse
- type AuditMeta
- type Authenticator
- type CORSPolicy
- type DenyAllAuthenticator
- type Health
- type HealthCheck
- type IdempotencyConfig
- type Meta
- type Middleware
- func BodyLimit(maxBytes int64) Middleware
- func CORS(p CORSPolicy) Middleware
- func RateLimit(limiter RateLimiter, keyFn func(*http.Request) string, opts ...RateLimitOption) Middleware
- func Recover(logger *slog.Logger) Middleware
- func RequestID() Middleware
- func SecureHeaders(opts ...SecureHeadersOption) Middleware
- func Timeout(d time.Duration) Middleware
- type Operation
- type ProblemError
- type RateLimitOption
- type RateLimiter
- type Route
- type RouteMeta
- type Router
- type ScopeExtractor
- type SecureHeadersOption
- type TokenBucket
Constants ¶
const IdempotencyHeader = "Idempotency-Key"
IdempotencyHeader is the client header carrying the idempotency key.
Variables ¶
This section is empty.
Functions ¶
func BindAndValidate ¶
BindAndValidate decodes the body (strict) and then runs struct-tag validation, returning KindValidation with field errors on failure (blueprint 05 §1). maxBytes bounds the body.
func Chain ¶
func Chain(h http.Handler, mws ...Middleware) http.Handler
Chain applies middlewares so that the first listed runs outermost.
func DecodeJSON ¶
DecodeJSON strict-decodes the request body into T: unknown fields are rejected (typo defense), the body is size-capped, and exactly one JSON value is required. All failures map to KindValidation — a malformed body is a client error, never a 500. The raw body content is never echoed into the error (redaction discipline).
func ETagFrom ¶
ETagFrom renders a strong ETag from an aggregate's optimistic-lock version. Clients echo it back via If-Match on mutating requests.
func KeyByActor ¶
KeyByActor derives a per-principal, tenant-scoped bucket key so one tenant's callers can never share a limiter bucket with another's (cross-tenant DoS). It is stable per principal even for machine callers, whose audit CapacityID is uuid.Nil for every API-key / system / webhook actor — keying on that id alone would collapse ALL machine traffic across ALL tenants into one bucket.
The key is always prefixed with the bound tenant id, then the strongest available principal identifier: the user's capacity, else an api-key/system name, else the JWT subject (user id). If no principal identifier is available it falls back to per-IP — still tenant-prefixed when a tenant is bound — rather than a single shared bucket. Place RateLimit AFTER the authz gate to use this.
func KeyByIP ¶
KeyByIP keys on the client IP (RemoteAddr host). Behind the reference proxy, which sets X-Real-IP / X-Forwarded-For, a product that trusts its proxy should supply a keyFn reading the forwarded header instead.
func ParseFilters ¶
ParseFilters collects filter.<field>=<op>:<value> query params and parses them against the allowlist. Unknown fields or disallowed operators are KindValidation errors — the client can only ever reference allowlisted columns, and values always become bound parameters.
func ParsePagination ¶
func ParsePagination(r *http.Request, def pagination.Defaults) (pagination.Request, error)
ParsePagination reads per_page + cursor and returns the clamped page request.
func ParseResourceID ¶
ParseResourceID reads a path parameter (net/http 1.22 pattern wildcard) and parses it as a UUID. A missing or malformed id is KindValidation.
func RequestHash ¶
RequestHash produces the stable hash stored with an idempotency key so a retry with the SAME key but a DIFFERENT request is rejected (409). Callers pass the canonical bytes of the decoded+validated command (the raw body is already consumed by decoding), which the kernel binds to method, path, AND query string (SEC-19: two requests differing only in query params must not share a stored response). Callers MUST pass deterministic, non-empty canonical bytes; an empty canonical weakens the different-request check.
func RequestIDFrom ¶
RequestIDFrom returns the request id, or "" if none was set.
func RequireIfMatch ¶
RequireIfMatch parses the mandatory If-Match header into the version the client last saw. A missing header is KindValidation (the client must opt into optimistic concurrency); a malformed one is likewise a client error. The caller compares the returned version against the row's version and returns KindVersionConflict (412) on mismatch.
The wildcard "If-Match: *" is intentionally NOT accepted: wowapi's optimistic concurrency requires the client to assert the concrete version it observed, so "match any" would defeat the guard it opts into (review finding ARCH-34).
func WithActor ¶
WithActor returns a context carrying the full authenticated principal. The authz gate sets it after AuthN so downstream middleware/helpers (e.g. KeyByActor) can identify the caller by its strongest identifier — not just the audit CapacityID bound via database.WithActorID, which is uuid.Nil for every machine principal.
func WithIdempotency ¶
func WithIdempotency(ctx context.Context, w http.ResponseWriter, r *http.Request, tx database.TxManager, cfg IdempotencyConfig, requestHash string, op Operation)
WithIdempotency executes op at most once per (tenant, actor, Idempotency-Key) and writes the response. Without the header it simply runs op in one tenant transaction. With the header it replays a stored response on retry, rejects a reused key carrying a different request (409 conflict), and rejects a still in-flight duplicate (409 retry_later) — the key claim, the business writes, and the stored response all commit in the same transaction (blueprint 05 §1–2). It owns the response so replay is transparent to the handler.
func WithRequestID ¶
WithRequestID returns a context carrying the request correlation id.
func WriteError ¶
func WriteError(ctx context.Context, w http.ResponseWriter, err error)
WriteError translates any error into a problem-details response. An *Error contributes its Kind (→ status/code), user-safe Msg, and field errors; any other error is rendered as an opaque 500 whose cause never reaches the wire (blueprint 04 §5). Observability logging is wired by the recover/log middleware; this function only shapes the response.
Types ¶
type APIResponse ¶
APIResponse is the success envelope. Data is the resource DTO; Meta is optional (request id, audit info).
func OK ¶
func OK[T any](data T) APIResponse[T]
OK wraps a DTO in the success envelope with no meta.
func OKWithMeta ¶
func OKWithMeta[T any](data T, meta *Meta) APIResponse[T]
OKWithMeta wraps a DTO with meta.
type AuditMeta ¶
type AuditMeta struct {
CreatedAt time.Time `json:"created_at"`
CreatedBy uuid.UUID `json:"created_by"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
Version int `json:"version"`
}
AuditMeta echoes the persisted audit columns; clients return Version via If-Match for optimistic concurrency.
type Authenticator ¶
Authenticator resolves a request's authenticated actor (carrying its tenant). The identity/tenant strategy (OIDC verification, tenant resolution) is deployment-specific, so a product supplies the concrete implementation; the framework never hardcodes the identity source. A returned error becomes the HTTP response (return a KindUnauthenticated error for a 401).
func Composite ¶
func Composite(auths ...Authenticator) Authenticator
Composite tries each authenticator in order and returns the first successful actor — the way a product runs API-key and OIDC auth side by side (roadmap S1/CA-2). An authenticator that returns a KindUnauthenticated error is treated as "not my scheme" and the next is tried; any OTHER error (e.g. the key store is unreachable) short-circuits so a transient fault is not misreported as a clean 401. If every authenticator declines, the last unauthenticated error is returned. With no authenticators it fails closed.
type CORSPolicy ¶
type CORSPolicy struct {
AllowedOrigins []string // exact-match origins; no wildcards (deny-by-default)
AllowedMethods []string // default: GET, POST, PUT, PATCH, DELETE, OPTIONS
AllowedHeaders []string // default: Authorization, Content-Type, X-Request-Id, Idempotency-Key
ExposedHeaders []string // response headers the browser may read
AllowCredentials bool // send Access-Control-Allow-Credentials
MaxAge time.Duration
}
CORSPolicy is the per-environment CORS allowlist (blueprint 07 §1). The zero policy allows no origins — CORS is deny-by-default, consistent with the rest of the framework. Products load the allowlist from their env config.
type DenyAllAuthenticator ¶
type DenyAllAuthenticator struct{}
DenyAllAuthenticator rejects every request as unauthenticated. It is the SECURE DEFAULT: a freshly-scaffolded API enforces deny-by-default (every non-Public route → 401) until a real Authenticator is wired, rather than serving business routes unguarded.
func (DenyAllAuthenticator) Authenticate ¶
Authenticate always fails closed.
type Health ¶
type Health struct {
// contains filtered or unexported fields
}
Health aggregates readiness checks and serves the two health endpoints.
func NewHealth ¶
NewHealth builds a health aggregator. fingerprint is the redacted config fingerprint (a hash — safe to expose) reported by /readyz.
func (*Health) Liveness ¶
func (h *Health) Liveness() http.HandlerFunc
Liveness answers 200 as long as the process is running — it runs NO checks (a failing dependency must not make the process get killed by a liveness probe; that is readiness' job).
func (*Health) Readiness ¶
func (h *Health) Readiness() http.HandlerFunc
Readiness runs every check (each bounded by checkTimeout) and returns 200 when all pass, 503 otherwise, with a per-check status map + the config fingerprint.
type HealthCheck ¶
HealthCheck reports readiness for one subsystem; a non-nil error = not ready.
type IdempotencyConfig ¶
type IdempotencyConfig struct {
Store database.IdemStore
ActorScope string // capacity id / system actor; scopes the key
TTL time.Duration // how long a stored response is replayable
}
IdempotencyConfig configures WithIdempotency.
type Middleware ¶
Middleware wraps an http.Handler. The kernel provides the cross-cutting concerns (request id, panic recovery, error-safe logging); auth/tenant middleware is added in Phase 4.
func BodyLimit ¶
func BodyLimit(maxBytes int64) Middleware
BodyLimit caps the request body at maxBytes by wrapping r.Body in an http.MaxBytesReader. Any read past the limit fails with *http.MaxBytesError, which DecodeJSON maps to 413; handlers that read the body directly see the same error. maxBytes <= 0 disables the cap.
func CORS ¶
func CORS(p CORSPolicy) Middleware
CORS enforces an origin allowlist. A request whose Origin is on the list gets that exact origin echoed back (never "*", so it composes with credentials); a preflight (OPTIONS + Access-Control-Request-Method) is answered with 204 and never reaches the handler. Requests from disallowed origins are served without CORS headers — the browser, not the server, enforces the block.
func RateLimit ¶
func RateLimit(limiter RateLimiter, keyFn func(*http.Request) string, opts ...RateLimitOption) Middleware
RateLimit rejects requests that exceed the limiter with 429 + Retry-After. The keyFn derives the bucket key from the request (see KeyByIP / KeyByActor). A nil keyFn defaults to KeyByIP.
func Recover ¶
func Recover(logger *slog.Logger) Middleware
Recover converts a panic into a 500 problem response: the stack goes to the logger and a panic metric, never to the wire (blueprint 04 §5). It must be the outermost middleware around handlers.
func RequestID ¶
func RequestID() Middleware
RequestID assigns a correlation id (honoring an inbound X-Request-Id when present) and stores it in the context and the response header, so every log line and problem body can be correlated.
func SecureHeaders ¶
func SecureHeaders(opts ...SecureHeadersOption) Middleware
SecureHeaders sets the baseline response security headers on every response (blueprint 07 §1): nosniff, frame-ancestors 'none', HSTS, and a strict Referrer-Policy. Headers are set before the handler runs so they are present even on handler-written error responses.
func Timeout ¶
func Timeout(d time.Duration) Middleware
Timeout enforces a per-request deadline. On expiry the request context is cancelled (so in-flight DB work aborts) and a 503 is written. d <= 0 disables the timeout. Built on http.TimeoutHandler, which buffers the handler's response so a late write cannot corrupt the timeout reply.
type Operation ¶
Operation is a mutating handler body: it runs inside a tenant transaction and returns the HTTP status and response DTO to write (and store, when idempotent). Returning an error rolls the transaction back and nothing is stored.
type ProblemError ¶
type ProblemError struct {
Type string `json:"type"`
Title string `json:"title"`
Status int `json:"status"`
Detail string `json:"detail,omitempty"`
Instance string `json:"instance,omitempty"`
Code string `json:"code"`
RequestID string `json:"request_id"`
Errors []errors.FieldError `json:"errors,omitempty"`
}
ProblemError is the RFC 9457 problem-details body — the ONLY error shape the API emits (blueprint 04 §4). It never carries internal detail: Op, wrapped causes, and stack traces stay in logs.
type RateLimitOption ¶
type RateLimitOption func(*rateLimitCfg)
RateLimitOption customizes the RateLimit middleware.
func OnRateLimitDrop ¶
func OnRateLimitDrop(fn func(route string)) RateLimitOption
OnRateLimitDrop registers a callback fired whenever a request is rejected (429). The composition root wires this to a metrics counter — httpx must not import kernel/observability (observability imports httpx), so the emission is injected as a plain callback (roadmap CA-1). route is r.Pattern.
type RateLimiter ¶
RateLimiter decides whether a request keyed by `key` may proceed now. When it may not, retryAfter is a hint for the Retry-After header.
type Route ¶
type Route struct {
Method string
Pattern string
Meta RouteMeta
Handler http.HandlerFunc
}
Route is a registered route with its metadata, exposed for permission-sync and OpenAPI generation (later phases).
type RouteMeta ¶
type RouteMeta struct {
// Permission is the permission key required to call the route. Empty only
// when Public is true.
Permission string
// Public opts a route out of authz (health, pre-verification webhooks).
Public bool
// Scope derives the authz target from the request (optional).
Scope ScopeExtractor
// Idempotent enables idempotency-key handling for unsafe methods.
Idempotent bool
// Sensitive forces an audit record even for reads.
Sensitive bool
}
RouteMeta is mandatory metadata for every route. There is deliberately no registration path without it: a route is either guarded by a Permission or explicitly Public, never neither and never both (blueprint 05 §1).
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router collects routes with enforced metadata. Registration errors are accumulated and surfaced by Err/Build, so application boot fails with the full list (consistent with config/app validation).
func (*Router) Err ¶
Err returns the accumulated registration errors joined, or nil. Callers (app boot) must check this before serving.
func (*Router) Handle ¶
func (r *Router) Handle(method, pattern string, meta RouteMeta, h http.HandlerFunc)
Handle registers a route. Invalid metadata (or a duplicate method+pattern) records an error retrievable via Err() — it does not panic, so a module's whole route set is validated at once.
func (*Router) Permissions ¶
Permissions returns the set of non-empty permission keys the routes require, sorted — the input to the Phase 4 permission-registration sync.
func (*Router) Routes ¶
Routes returns the registered routes in a deterministic order (for permission sync, OpenAPI, and tests).
func (*Router) SecureHandler ¶
func (r *Router) SecureHandler(auth Authenticator, eval authz.Evaluator, txm database.TxManager) *http.ServeMux
SecureHandler builds the serving mux with every route wrapped by the authN→authZ(RouteMeta) gate. Public routes are served directly; every other route is authenticated and authorized against its declared permission before its handler runs. Health endpoints (added by the caller after this) are Public infrastructure and are mounted directly.
type ScopeExtractor ¶
ScopeExtractor derives the authorization target (org/resource id) from a request; the auth middleware (Phase 5) passes it to authz.Evaluate. Returning an error fails the request as a client error (review finding ARCH-43 — now typed to authz.Target).
type SecureHeadersOption ¶
type SecureHeadersOption func(*secureHeadersConfig)
SecureHeadersOption tunes the SecureHeaders middleware.
func WithCSP ¶
func WithCSP(value string) SecureHeadersOption
WithCSP overrides the Content-Security-Policy value. The default only asserts frame-ancestors 'none' (clickjacking defense for a JSON API); products that serve HTML should set a fuller policy.
func WithHSTS ¶
func WithHSTS(value string) SecureHeadersOption
WithHSTS overrides the Strict-Transport-Security value.
func WithoutHSTS ¶
func WithoutHSTS() SecureHeadersOption
WithoutHSTS disables the Strict-Transport-Security header — use only where TLS is not terminated in front of the service (e.g. local plain-HTTP).
type TokenBucket ¶
type TokenBucket struct {
// contains filtered or unexported fields
}
TokenBucket is an in-memory per-key token-bucket RateLimiter. Each key refills at `rate` tokens/sec up to `burst`. Idle buckets are swept so the map cannot grow without bound under a spray of distinct keys.
func NewTokenBucket ¶
func NewTokenBucket(ratePerSec float64, burst int) *TokenBucket
NewTokenBucket builds a limiter of rate tokens/sec with the given burst.
func NewTokenBucketWithClock ¶
func NewTokenBucketWithClock(ratePerSec float64, burst int, now func() time.Time) *TokenBucket
NewTokenBucketWithClock is NewTokenBucket with an injectable clock (tests).