Documentation
¶
Overview ¶
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Code generated by apic; DO NOT EDIT.
Package httpx provides net/http server building blocks and middleware for apic services: a TLS/mTLS listener with hardened timeouts and body-size limits, transparent gzip response compression (with BREACH-sensitive path exclusions), token-bucket rate limiting (a coarse per-IP abuse ceiling plus stricter per-path-prefix and per-peer buckets), CORS handling, and the default-deny security response-header set (CSP, COOP/COEP/CORP, Permissions-Policy). It depends only on the standard library plus the internal rate-bucket and obsx observability helpers. Code generated by apic; DO NOT EDIT.
Index ¶
- Variables
- func DecodeJSON(r io.Reader, v any) error
- func Gzip(next http.Handler) http.Handler
- func GzipWithExcludes(next http.Handler, excludes []string) http.Handler
- func RouteLimiter(rate, burst float64, next http.Handler) http.Handler
- func Serve(ctx context.Context, cfg Config, mux *http.ServeMux) error
- func ValidateConfig(cfg Config) error
- func ValidateOrigins(origins []string) error
- func WrapHandler(cfg Config, h http.Handler) http.Handler
- func WrapHandlerCtx(ctx context.Context, cfg Config, h http.Handler) (http.Handler, io.Closer)
- type Config
- type IPSource
- type Mux
- type PathLimit
- type PathPeerLimiter
- type PeerLimiter
Constants ¶
This section is empty.
Variables ¶
var ( ErrInvalidConfig = errors.New("httpx: invalid configuration") ErrAuthFailed = errors.New("httpx: authentication failed") ErrRateLimited = errors.New("httpx: rate limit exceeded") ErrBodyTooLarge = errors.New("httpx: request body exceeds configured limit") // ErrIncompleteTLSConfig is returned when exactly one of TLSCert and // TLSKey is configured. Falling back to plaintext in that state would be // an unsafe downgrade, so server startup fails closed instead. ErrIncompleteTLSConfig = errors.New("httpx: TLS certificate and key must be configured together") // ErrInvalidTrustedProxyCIDR is returned by SetTrustedProxies (and the // construction paths that consume TrustedProxyCIDRs) when an entry is not // a parseable CIDR. T-06. ErrInvalidTrustedProxyCIDR = errors.New("httpx: invalid trusted-proxy CIDR") )
Sentinel errors for HTTP runtime. Kept at package level per repo conventions. Messages use a lowercase, package-prefixed, diagnostic form so log output reads naturally; errors.Is callers should rely on pointer equality rather than substring matching of Error().
var ErrCORSWildcardWithCredentials = errors.New("httpx: CORS wildcard origin \"*\" is forbidden when CORS is enabled; list explicit origins or disable CORS")
ErrCORSWildcardWithCredentials is returned by ValidateConfig when CORS is enabled and any configured origin is the literal "*". Allowing a wildcard origin while a server also accepts credentials (cookies, Authorization headers, mTLS client certs) is forbidden by the CORS spec (https://fetch.spec.whatwg.org/#cors-protocol-and-credentials) and is a well-known misconfiguration that nullifies the same-origin protection of any session cookie or bearer token the server issues. We refuse the configuration at startup so the misconfiguration is discoverable in CI instead of silently shipping to production. Disable CORS or list explicit origins instead.
var ErrInvalidOrigin = errors.New("httpx: invalid CORS origin")
ErrInvalidOrigin is returned by ValidateOrigins for a malformed CORS origin.
Functions ¶
func DecodeJSON ¶
DecodeJSON reads JSON from r into v, rejecting unknown fields.
func Gzip ¶
Gzip wraps next with transparent response compression. It compresses only when the client sent Accept-Encoding: gzip, the response Content-Type is text-ish (JSON, text/*, JS, CSS, SVG, XML), the body is at least ~1 KiB, and the handler did not already set Content-Encoding. A Vary: Accept-Encoding header is added on every response that went through the compressible path so shared caches key correctly. The gzip writers are pooled at BestSpeed; the wrapper struct itself is pooled too (PERF #163) so the steady-state allocation per request is 0.
func GzipWithExcludes ¶
GzipWithExcludes is Gzip with an additional list of request paths whose responses must never be compressed. A-NEW-1: callers list BREACH-sensitive endpoints (e.g. /v1/oauth/token, /v1/login, anything emitting bearer tokens or session cookies) so a misbehaving handler that forgets to set Cache-Control: no-store still cannot leak through gzip+TLS. Matching is exact, with a trailing-slash tolerance: an entry "/v1/oauth/token" matches both "/v1/oauth/token" and "/v1/oauth/token/". No regex, no wildcards. Passing a nil or empty excludes slice is equivalent to Gzip.
func RouteLimiter ¶
RouteLimiter applies a token bucket per route and sets X-RateLimit-* headers.
NP-12 / semantic change: the reported X-RateLimit-Remaining is now the POST-ADMIT snapshot (the bucket's state immediately after this request's token was consumed, or immediately after a denied request's failed admit attempt), not the pre-admit snapshot taken before the token was consumed. The two-call sequence this replaced -- Snapshot() (read remaining) then Allow() (consume a token) -- took the bucket mutex twice and reported a value that was stale by exactly one token on every response, ever since the aggregate/per-route generated handlers were already migrated to the same AllowAndSnapshot atomic admit-and-read (see api.go.tmpl / PERF #167); this aligns the shared library helper with that already-adopted contract instead of leaving it as the one inconsistent caller. AllowAndSnapshot also takes the mutex ONCE per request instead of twice.
func ValidateConfig ¶
ValidateConfig returns a non-nil error if cfg would silently downgrade a partially configured TLS server to plaintext, or if its CORS configuration would let cross-origin callers from any host read responses while the server also presents auth-bearing handlers. Callers should invoke this before passing cfg to WrapHandler or Serve. WrapHandler and Serve both call it internally as a hard guard; ValidateConfig is exported so configuration loaders and tests can invoke the same validation.
func ValidateOrigins ¶
ValidateOrigins checks a CORS origin allowlist for boot-time safety (used by the APIC_CORS_ORIGINS runtime override). Each origin must be a non-empty absolute URL (scheme://host) with no whitespace/control chars, and must not be the wildcard "*" (ValidateConfig forbids "*" when CORS is enabled). An empty list is valid.
func WrapHandler ¶
WrapHandler wraps h with security headers, CORS, and body size limiting per cfg. Use this when managing the http.Server directly instead of calling Serve.
WrapHandler panics if cfg fails ValidateConfig. CORS misconfiguration is a startup-only condition; we choose to fail loud at startup so CI catches the regression rather than silently shipping a CORS policy that defeats the purpose of the rest of the security stack.
func WrapHandlerCtx ¶
WrapHandlerCtx is the leak-free variant of WrapHandler (QG-084). It returns the same wrapped handler plus a closer that stops the background sweeper goroutines owned by the per-IP and per-path rate limiters. When ctx is non-nil the closer is invoked automatically on ctx.Done(); callers managing their own *http.Server should pass the server's lifecycle context here instead of calling WrapHandler, so the limiter goroutines are released at shutdown. The returned closer is also returned directly so a caller without a context can defer it. Calling the closer more than once is safe.
WrapHandlerCtx panics if cfg fails ValidateConfig, identically to WrapHandler.
Types ¶
type Config ¶
type Config struct {
Bind string
TLSCert string
TLSKey string
MTLSCA string
ReadHeader time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
MaxBodyBytes int64
CORSEnabled bool
CORSOrigins []string
// CORSAllowAnyOrigin is the explicit any-origin opt-in (T-05). By
// default a literal "*" entry in CORSOrigins is NOT treated as
// match-all: ValidateConfig rejects it (ErrCORSWildcardWithCredentials)
// and the headers() matcher ignores it. Setting this flag to true
// permits "*" and makes the CORS matcher reflect any request Origin
// into Access-Control-Allow-Origin. Default false (fail-closed). Use
// only for surfaces that intentionally serve any origin and carry no
// credentials; it is forbidden by the CORS spec to combine "*" with
// credentialed requests, so enable this knowingly.
CORSAllowAnyOrigin bool
// EnableCompression turns on transparent gzip of text-ish JSON
// responses (>=1 KiB; client must send Accept-Encoding: gzip).
EnableCompression bool
// CompressionExcludePaths lists request paths whose responses must
// never be gzipped, even when EnableCompression is true. Use for
// BREACH-sensitive routes (OAuth token endpoints, login, anything
// emitting bearer tokens or session cookies). Matching is exact with
// a trailing-slash tolerance (no regex, no wildcards). Empty/nil
// preserves today's behaviour (compress every eligible response).
CompressionExcludePaths []string
// PerIPRPS / PerIPBurst configure a coarse global token bucket
// partitioned by client IP, applied to every route as an abuse
// ceiling underneath any tighter per-route limiters. Zero disables.
PerIPRPS float64
PerIPBurst float64
// PerIPSource selects the key extractor for the per-IP limiter.
PerIPSource IPSource
// TrustedProxyCIDRs lists the reverse-proxy networks whose
// X-Forwarded-For / X-Real-IP headers the global per-IP limiter will
// honor when PerIPSource is a proxy-header source. Empty (the default)
// trusts no proxy, so forged forwarded headers are ignored and the
// limiter keys on RemoteAddr -- forged XFF cannot dodge per-IP limits.
// Set this only when the server sits behind a reverse proxy that
// overwrites (not appends to) the header. Additive/default-safe. T-06.
TrustedProxyCIDRs []string
// PathRateLimits install stricter per-IP buckets for matching path
// prefixes (F3), applied as the outermost layer (before the global per-IP
// limiter) so e.g. /v1/auth/* gets a tighter credential-stuffing ceiling.
PathRateLimits []PathLimit
// Security response headers (SEC-0023). The hardened set —
// Content-Security-Policy, Cross-Origin-Opener/Embedder/Resource-Policy,
// and Permissions-Policy — is emitted by default with deny-by-default
// values suitable for a JSON API. For each field: an empty string emits
// the secure default; a non-empty string overrides it; the literal "-"
// suppresses that single header. DisableSecurityHeaders drops the whole
// hardened set at once (the legacy HSTS/X-Content-Type-Options/
// X-Frame-Options/Referrer-Policy headers are always emitted regardless).
ContentSecurityPolicy string
CrossOriginOpenerPolicy string
CrossOriginEmbedderPolicy string
CrossOriginResourcePolicy string
PermissionsPolicy string
DisableSecurityHeaders bool
}
Config holds runtime server settings.
type IPSource ¶
type IPSource int
IPSource selects which request attribute the global per-IP rate limiter keys on. RemoteAddr is the only trustworthy default; the proxy-header variants must only be used when the server sits behind a trusted reverse proxy that overwrites (not appends to) the header, otherwise a client can forge the key and dodge the limiter.
const ( // IPSourceRemoteAddr keys on the TCP peer address (default, safe). IPSourceRemoteAddr IPSource = iota // IPSourceXForwardedFor keys on the left-most X-Forwarded-For entry. IPSourceXForwardedFor // IPSourceXRealIP keys on the X-Real-IP header. IPSourceXRealIP // IPSourceTLSSubject keys on the verified client-cert Subject (mTLS). IPSourceTLSSubject )
func ParseIPSource ¶
ParseIPSource maps a config string to an IPSource. Unknown values fall back to IPSourceRemoteAddr.
type Mux ¶
Mux returns a new ServeMux and a helper to mount JSON routes with rate limits.
func (*Mux) HandleJSON ¶
func (m *Mux) HandleJSON(path, method string, h func(http.ResponseWriter, *http.Request), rate, burst float64)
HandleJSON registers a JSON route with optional rate limiting.
type PathLimit ¶
type PathLimit struct {
Prefix string
RPS float64
Burst int
Source IPSource
// TrustedProxyCIDRs lists the reverse-proxy networks whose forwarded
// headers this path's limiter will honor when Source is a proxy-header
// source. Empty (the default) trusts no proxy, so forged X-Forwarded-For
// / X-Real-IP values are ignored and keying falls back to RemoteAddr.
// Additive/optional so existing construction stays default-safe. T-06.
TrustedProxyCIDRs []string
}
PathLimit configures a stricter per-IP bucket for one path prefix (F3).
type PathPeerLimiter ¶
type PathPeerLimiter struct {
// contains filtered or unexported fields
}
PathPeerLimiter applies per-IP buckets scoped to path prefixes, longest prefix first. A request matching no prefix passes through untouched (the global per-IP limiter, if any, still applies upstream).
func NewPathPeerLimiter ¶
func NewPathPeerLimiter(limits []PathLimit) *PathPeerLimiter
NewPathPeerLimiter builds a limiter from the configured path limits. Entries with non-positive RPS and Burst are skipped (disabled). Prefixes are sorted longest-first so the most specific match wins.
func (*PathPeerLimiter) Close ¶
func (p *PathPeerLimiter) Close() error
Close stops every underlying per-path PeerLimiter's background sweeper goroutine (QG-084). NewPathPeerLimiter spawns one sweeper per configured path limit; without Close those goroutines leak for the life of the process. Safe to call on a nil or empty PathPeerLimiter, and safe to call more than once (each PeerLimiter.Close is itself once-safe). Always returns nil; the error return matches the io.Closer shape so callers can wire it into a context-cancel closer alongside other resources.
func (*PathPeerLimiter) Empty ¶
func (p *PathPeerLimiter) Empty() bool
Empty reports whether no path limits are configured.
func (*PathPeerLimiter) Middleware ¶
func (p *PathPeerLimiter) Middleware(next http.Handler) http.Handler
Middleware wraps next so the first matching path-prefix bucket gates the request (429 on reject). Non-matching requests pass through.
type PeerLimiter ¶
type PeerLimiter struct {
// contains filtered or unexported fields
}
PeerLimiter is a global token-bucket rate limiter partitioned by client IP (or another configured key source). It is meant to sit at the very outside of the handler chain as a coarse abuse ceiling underneath any tighter per-route or per-actor limiters. Buckets idle longer than the eviction TTL are swept by a background goroutine so a flood of unique source addresses cannot pin memory beyond the maxEntries ceiling (see SetMaxEntries) even between sweeps.
func NewPeerLimiter ¶
func NewPeerLimiter(rate, burst float64, src IPSource) *PeerLimiter
NewPeerLimiter constructs a per-IP limiter. rate is tokens/second, burst the bucket size. src selects the key extractor. The returned limiter starts a sweeper goroutine; call Close to stop it. A rate of zero (or negative) is the caller's signal to skip wiring the middleware entirely; NewPeerLimiter still returns a usable allow-all limiter for safety.
func (*PeerLimiter) Close ¶
func (pl *PeerLimiter) Close()
Close stops the background sweeper. Idempotent and safe to call concurrently.
N-02: this previously used a select/default/close, which is a check-then-act race — two goroutines could both find stop open and both close it, panicking with "close of closed channel". WrapHandlerCtx manufactures exactly that case: it spawns a goroutine that closes on ctx.Done() while also returning the closer for the caller to defer. The once-safety promised by the PathPeerLimiter.Close and WrapHandlerCtx docs now actually holds.
func (*PeerLimiter) Middleware ¶
func (pl *PeerLimiter) Middleware(next http.Handler) http.Handler
Middleware wraps next so every request first passes the per-IP bucket. On rejection it emits a 429 with Retry-After and a JSON body shaped {code, description, reason} -- the same envelope the generated per-route limiters use -- so a single client-side 429 handler covers all layers.
Callers gate wiring on rate>0 && burst>0 (a rate of zero is the "disabled" signal); Middleware itself does not self-disable so tests can drive a zero-refill bucket.
func (*PeerLimiter) SetMaxEntries ¶ added in v0.17.0
func (pl *PeerLimiter) SetMaxEntries(n int)
SetMaxEntries overrides the default entry-count ceiling (NP-06/06b). n<=0 restores the default (defaultMaxPeerEntries); it never disables the cap entirely -- an unbounded PeerLimiter is exactly the vulnerability this closes. Intended to be called once at construction, before the limiter is wired into a handler chain (like SetTrustedProxies); safe to call later too since it only guards new-key admission going forward.
func (*PeerLimiter) SetTrustedProxies ¶
func (pl *PeerLimiter) SetTrustedProxies(cidrs []string) error
SetTrustedProxies configures the reverse-proxy CIDR networks whose X-Forwarded-For / X-Real-IP headers this limiter will honor for keying. Each entry must be a parseable CIDR (e.g. "10.0.0.0/8", "::1/128"); an invalid entry returns ErrInvalidTrustedProxyCIDR and leaves the existing trust set unchanged. Passing an empty slice clears the set (no proxy trusted -> forwarded headers ignored, the default-safe state). Intended to be called once at construction, before the limiter is wired into a handler chain. T-06.