httpx

package
v0.15.3 Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

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.

Index

Constants

This section is empty.

Variables

View Source
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")
	// 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().

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

View Source
var ErrInvalidOrigin = errors.New("httpx: invalid CORS origin")

ErrInvalidOrigin is returned by ValidateOrigins for a malformed CORS origin.

Functions

func DecodeJSON

func DecodeJSON(r io.Reader, v any) error

DecodeJSON reads JSON from r into v, rejecting unknown fields.

func Gzip

func Gzip(next http.Handler) http.Handler

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

func GzipWithExcludes(next http.Handler, excludes []string) http.Handler

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

func RouteLimiter(rate, burst float64, next http.Handler) http.Handler

RouteLimiter applies a token bucket per route and sets X-RateLimit-* headers.

func Serve

func Serve(ctx context.Context, cfg Config, mux *http.ServeMux) error

Serve starts an HTTP server with TLS1.3-only and sane defaults.

func ValidateConfig

func ValidateConfig(cfg Config) error

ValidateConfig returns a non-nil error if cfg encodes a CORS configuration that would let cross-origin callers from any host read responses while the server also presents auth-bearing handlers (the common case in this generator's output: JWT, API-key, and mTLS auth all attach credentials to requests that CORS can replay). 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 it can also be invoked from configuration loaders and tests.

func ValidateOrigins

func ValidateOrigins(origins []string) error

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

func WrapHandler(cfg Config, h http.Handler) http.Handler

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

func WrapHandlerCtx(ctx context.Context, cfg Config, h http.Handler) (http.Handler, io.Closer)

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

func ParseIPSource(s string) IPSource

ParseIPSource maps a config string to an IPSource. Unknown values fall back to IPSourceRemoteAddr.

type Mux

type Mux struct{ *http.ServeMux }

Mux returns a new ServeMux and a helper to mount JSON routes with rate limits.

func NewMux

func NewMux() *Mux

NewMux constructs a new mux wrapper.

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.

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. Safe to call once.

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) 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.

Jump to

Keyboard shortcuts

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