Documentation
¶
Overview ¶
Package middleware is part of the GoFastr framework. See https://github.com/DonaldMurillo/gofastr for documentation.
Package middleware provides HTTP middleware primitives for GoFastr.
The core type is Middleware: func(next http.Handler) http.Handler. Middleware can be composed with Chain and assembled into a Pipeline.
Index ¶
- Constants
- Variables
- func DevCSRFKeyFromFile(path string) ([]byte, error)
- func GetRequestID(ctx context.Context) string
- func MetricsHandler(m *Metrics) http.Handler
- func SkipAny(predicates ...func(*http.Request) bool) func(*http.Request) bool
- func SkipBearerAuth() func(*http.Request) bool
- func SpanFromRequest(r *http.Request) trace.Span
- func TokenFromContext(ctx context.Context) string
- func WarnIfCSRFUnconfigured(cfg CSRFConfig, logger interface{ ... })
- type CORSConfig
- type CSRFConfig
- type CSRFSkipper
- type IdempotencyConfig
- type IdempotencyStore
- type IdempotentResponse
- type MemoryIdempotencyOption
- type Metrics
- type Middleware
- func CORS(cfg CORSConfig) Middleware
- func CSRF(cfg CSRFConfig) Middleware
- func Chain(mw ...Middleware) Middleware
- func DiscardLogging() Middleware
- func Idempotency(cfg IdempotencyConfig) Middleware
- func Logging() Middleware
- func LoggingFn(getLogger func() *slog.Logger) Middleware
- func LoggingWithWriter(w io.Writer) Middleware
- func MetricsMiddleware(m *Metrics) Middleware
- func RateLimit(cfg RateLimitConfig) Middleware
- func Recovery() Middleware
- func RecoveryFn(getLogger func() *slog.Logger) Middleware
- func RequestID() Middleware
- func SampledLogging(sampleN int, slowThreshold time.Duration) Middleware
- func SampledLoggingFn(sampleN int, slowThreshold time.Duration, getLogger func() *slog.Logger) Middleware
- func SecurityHeaders(cfg SecurityHeadersConfig) Middleware
- func Timeout(d time.Duration) Middleware
- func Tracing() Middleware
- type Pipeline
- type RateLimitConfig
- type SQLIdempotencyOption
- type SQLIdempotencyStore
- type SecurityHeadersConfig
Constants ¶
const (
// HeaderRequestID is the HTTP header used to convey the request ID.
HeaderRequestID = "X-Request-ID"
)
const IdempotencyKeyHeader = "Idempotency-Key"
IdempotencyKeyHeader is the request header clients use to assign a stable identity to a write. Two requests carrying the same value are the "same" request from the client's point of view; the middleware guarantees at-most-once side-effects within the store's retention window.
const MaxRequestIDLen = 128
MaxRequestIDLen caps client-supplied X-Request-ID values. Above this length the inbound header is rejected and a fresh ID generated.
Without a cap, an attacker can stuff arbitrary multi-KB strings into every request — they'd be logged in every access entry, reflected on the response, and amplified across webhook sinks.
Variables ¶
var ( ErrFingerprintMismatch = errors.New("idempotency: key reused with different request") ErrInFlight = errors.New("idempotency: concurrent request in flight") )
Sentinel errors returned by IdempotencyStore.Begin.
Functions ¶
func DevCSRFKeyFromFile ¶
DevCSRFKeyFromFile returns 32 bytes of HMAC key for use as CSRFConfig.SecretKey. On first run the file at path doesn't exist; the helper generates fresh random bytes, writes them (mode 0600), and returns the same value to the caller. On subsequent runs the helper reads the existing file and returns its contents.
This is the V3 #5 fix for dev-mode UX: without a stable key, every dev-server restart rotates the per-process auto-key and any browser tab with a stale cookie gets a 403 on its next form submit. Persisting the key to disk lets the cookie survive restarts.
INTENDED FOR DEV ONLY. The file's contents ARE the signing key, so any process that can read the file can forge CSRF tokens against this app. In production, source the key from your secret manager and pass it via CSRFConfig.SecretKey directly.
Path is created with its parent directory if missing. The directory is created with mode 0700, the file with mode 0600. The file is NOT gitignored automatically — the caller's repo .gitignore should exclude .gofastr/ (or wherever they chose) to keep the key out of version control.
Returns an error if the file exists but is unreadable, the wrong length, or the path can't be created. Callers in dev should treat this as fatal — logging a warning and falling back to the auto-key reintroduces exactly the UX problem this helper exists to solve.
func GetRequestID ¶
GetRequestID retrieves the request ID from the given context. Returns an empty string if no request ID is present.
func MetricsHandler ¶
MetricsHandler returns an http.Handler that serves the metrics in Prometheus text exposition format. Mount at /metrics or anywhere the scrape config points.
func SkipAny ¶
SkipAny returns a Skip predicate that reports true when ANY of the passed predicates does. Lets hosts compose CSRFSkipper.Skip alongside SkipBearerAuth without writing their own boolean glue. A zero-arg call returns a predicate that always reports false (no skips).
func SkipBearerAuth ¶
SkipBearerAuth returns a Skip predicate suitable for CSRFConfig.Skip that bypasses requests using Authorization: Bearer or Api-Key headers — those don't ride on cookies and so aren't subject to CSRF.
func SpanFromRequest ¶
SpanFromRequest is a thin convenience that returns the active span on the request's context. Returns a no-op span if Tracing() isn't installed.
func TokenFromContext ¶
TokenFromContext returns the CSRF token stashed on ctx by the middleware, or "" when no token is available. Template helpers should prefer this over r.Cookie because the cookie isn't in the request on the GET that mints it.
func WarnIfCSRFUnconfigured ¶
func WarnIfCSRFUnconfigured(cfg CSRFConfig, logger interface { Warn(msg string, args ...any) })
WarnIfCSRFUnconfigured emits a WARN log when the given CSRFConfig relies on the per-process auto-generated SecretKey. Hosts should call this once at startup. Multi-instance deploys silently break without a shared SecretKey because every replica signs tokens with a different per-process key — verification fails when a request lands on a different replica than the one that minted the cookie.
Passing nil for logger uses slog.Default. Safe to call with any CSRFConfig; emits nothing when SecretKey is set.
Types ¶
type CORSConfig ¶
type CORSConfig struct {
// AllowedOrigins is the list of allowed origin patterns.
// Use "*" to allow all origins.
AllowedOrigins []string
// AllowedMethods is the list of allowed HTTP methods.
// Defaults to GET, POST, PUT, DELETE, PATCH, OPTIONS if empty.
AllowedMethods []string
// AllowedHeaders is the list of allowed request headers.
AllowedHeaders []string
}
CORSConfig holds configuration for the CORS middleware.
type CSRFConfig ¶
type CSRFConfig struct {
CookieName string
HeaderName string
CookiePath string
// CookieSecure marks the CSRF cookie Secure (HTTPS-only). Leave false
// for local dev; set true in production.
CookieSecure bool
// SecretKey is the HMAC key used to sign the CSRF token. Empty means
// the middleware autogenerates a per-process key on first use —
// fine for single-instance dev, NOT acceptable for production
// (each restart and each fleet replica gets a different key, so
// in-flight tokens silently 403). Call WarnIfCSRFUnconfigured at
// startup to surface this to operators.
SecretKey []byte
// AdditionalKeys lets a deploy rotate SecretKey without invalidating
// every in-flight form. The middleware signs new tokens with
// SecretKey but accepts tokens verified by SecretKey OR any key
// listed here. Drain the previous key from this list once all old
// tokens have expired.
AdditionalKeys [][]byte
// Skip allows the middleware to be bypassed for specific requests.
Skip func(*http.Request) bool
// FormField is the form-body field name read as a fallback when the
// request is form-encoded and the header is missing. Defaults to
// "_csrf". HTML form flows put the token in a hidden input with
// this name so the header doesn't need to be set client-side.
FormField string
// MaxFormBytes caps how much of a form-encoded request body the
// middleware will buffer when probing for FormField. Defaults to
// 1 MiB. Bodies above the cap return 413 before any allocation —
// without this, an unauthenticated attacker could force the
// process to buffer up to 10 MB (form-urlencoded) or 32 MB
// (multipart) per request just to land in the signature-mismatch
// branch. Set to a smaller value for endpoints that never carry
// large forms; do NOT set to 0 (interpreted as "use default").
MaxFormBytes int64
}
CSRFConfig configures the double-submit-cookie CSRF middleware.
CookieName / HeaderName must match what the client sends back; defaults are sensible.
Skip is consulted on every request — return true to bypass the check entirely (e.g., for endpoints authenticated by Bearer tokens or API keys, which aren't subject to CSRF since they don't ride on cookies).
SecretKey, when non-empty, switches the middleware to a signed-double- submit pattern: the cookie value is "<random>.<HMAC>" and the server rejects any request whose cookie/header lacks a valid signature. Without this, naive double-submit is vulnerable to cookie injection from sibling subdomains. Strongly recommended; defaults to a per-process random key if left empty (rotates on restart).
When SecretKey is set AND CookieSecure (or r.TLS) is true, the cookie name automatically gets the __Host- prefix, which forbids subdomain cookie injection at the browser level.
type CSRFSkipper ¶
type CSRFSkipper struct {
// contains filtered or unexported fields
}
CSRFSkipper accumulates path prefixes that should bypass the CSRF middleware. It composes into CSRFConfig.Skip via Skipper.Skip:
skipper := middleware.NewCSRFSkipper()
skipper.Add("/webhooks/", "/health")
mw := middleware.CSRF(middleware.CSRFConfig{
SecretKey: secret,
Skip: middleware.SkipAny(middleware.SkipBearerAuth(), skipper.Skip),
})
Adding paths after the middleware is constructed is safe — Skip reads under an RWMutex, so plugins / OnStart hooks can register per-route exemptions late and the next request honors them. This is the per-route-skip surface called out in V3 #9: hosts list their exemptions centrally instead of scattering closures that inspect r.URL.Path.
Path prefix matching is literal string-prefix (no globbing). A trailing "/" pins the prefix to a directory; without it a registered "/api" also skips "/apis/v1/...". Be deliberate — and prefer the trailing-slash form unless you specifically want the broader match.
func NewCSRFSkipper ¶
func NewCSRFSkipper() *CSRFSkipper
NewCSRFSkipper returns an empty skipper. Callers register prefixes with Add and pass Skip to CSRFConfig.Skip (typically via SkipAny).
func (*CSRFSkipper) Add ¶
func (s *CSRFSkipper) Add(prefixes ...string)
Add registers one or more path prefixes for CSRF bypass. Safe to call concurrently with Skip.
type IdempotencyConfig ¶
type IdempotencyConfig struct {
Store IdempotencyStore
TTL time.Duration
MaxBodyBytes int64
MaxResponseBytes int64
Methods []string
Required bool
Principal func(r *http.Request) string
FailOpen bool
}
IdempotencyConfig configures the idempotency middleware.
Store defaults to an in-memory store with TTL. Set this to a redis- or db-backed implementation for multi-instance deployments.
TTL controls how long completed responses are remembered. Default 24h (matches the Stripe/Square convention).
MaxBodyBytes caps how much of the request body is read for fingerprint + replay capture. Defaults to 1 MiB. Larger requests bypass idempotency to keep memory bounded — they receive a Vary header indicating the bypass but otherwise proceed normally.
MaxResponseBytes caps the size of the captured response body. When a successful handler writes more than this, the claim is released and the response goes through unchanged. Default 1 MiB.
Methods restricts which HTTP methods participate. Defaults to POST, PUT, PATCH, DELETE. GET/HEAD/OPTIONS always bypass.
Required, if true, rejects unsafe writes that don't carry the header (400). Default false — header is opt-in per request.
Principal extracts the authenticated subject (user/tenant id) from each request. When set, the fingerprint is namespaced by the result so two principals using the SAME Idempotency-Key value never see each other's cached responses — closing a cross-tenant replay leak. Default: empty principal (no namespacing); apps SHOULD wire one.
FailOpen flips behaviour on store error: true falls through to the handler (availability-first), false returns 503 to the client (correctness-first). Default false — a broken store no longer silently allows duplicate writes.
type IdempotencyStore ¶
type IdempotencyStore interface {
Begin(ctx context.Context, key, fingerprint string) (replay *IdempotentResponse, ok bool, err error)
Finish(ctx context.Context, key string, resp *IdempotentResponse) error
}
IdempotencyStore is the pluggable backend for cached responses. Implementations must be safe for concurrent use.
Begin claims a key. The semantics are:
- replay non-nil, ok=true: a cached response already exists for this key and fingerprint; the middleware should write replay back and skip the downstream handler.
- replay nil, ok=true: the caller is the first writer for this key. It must call Finish exactly once with the captured response.
- ok=false, err=ErrFingerprintMismatch: same key was used previously with a different request fingerprint. The middleware responds 422.
- ok=false, err=ErrInFlight: another request with the same key is currently executing. The middleware responds 409.
- any other err: storage failure; middleware fails closed (503) unless IdempotencyConfig.FailOpen is true.
func NewMemoryIdempotencyStore ¶
func NewMemoryIdempotencyStore(ttl time.Duration, opts ...MemoryIdempotencyOption) IdempotencyStore
NewMemoryIdempotencyStore returns an in-process IdempotencyStore. Suitable for single-instance deployments and tests. Use a Redis- or DB-backed implementation behind the same interface for clusters.
type IdempotentResponse ¶
IdempotentResponse is the cached snapshot of a completed write.
type MemoryIdempotencyOption ¶
type MemoryIdempotencyOption func(*memoryIdempotencyStore)
MemoryIdempotencyOption configures the in-process store.
func WithMemoryStoreMaxEntries ¶
func WithMemoryStoreMaxEntries(n int) MemoryIdempotencyOption
WithMemoryStoreMaxEntries caps the number of resident entries. When the cap is hit, the oldest entry (by creation time) is evicted to make room. Default is unlimited; set this when accepting traffic from anywhere a single attacker could submit unique keys forever.
type Metrics ¶
type Metrics struct {
// contains filtered or unexported fields
}
Metrics tracks per-(method, route, status) request counters and per-route latency histograms. Exposes a Prometheus-compatible text endpoint at /metrics (or wherever the caller mounts MetricsHandler).
Single instance per process; pass the *Metrics into MetricsMiddleware and MetricsHandler so both share the same store.
type Middleware ¶
Middleware is a function that wraps an http.Handler, returning a new one. It is the fundamental building block for HTTP middleware composition.
func CORS ¶
func CORS(cfg CORSConfig) Middleware
CORS returns middleware that adds CORS headers to responses. It handles preflight OPTIONS requests by returning 204 with the appropriate headers. When multiple AllowedOrigins are configured, the request's Origin header is matched against the list and the matching origin is echoed back (Access-Control-Allow-Origin only accepts a single value).
func CSRF ¶
func CSRF(cfg CSRFConfig) Middleware
CSRF returns a Middleware that enforces the double-submit cookie pattern:
- On safe methods (GET, HEAD, OPTIONS) the middleware sets a cookie containing a freshly-rotated token if none is present.
- On unsafe methods (POST, PUT, PATCH, DELETE) the middleware verifies that the header value matches the cookie value. Mismatch → 403.
This protects against cross-site form submissions because attacker- controlled pages can't read the cookie value to populate the header.
func Chain ¶
func Chain(mw ...Middleware) Middleware
Chain composes zero or more middleware into a single Middleware. The resulting middleware applies the input middleware in order: Chain(A, B, C)(handler) produces A(B(C(handler))). This means the first middleware's pre-processing runs first, and its post-processing runs last (A→B→C→handler→C→B→A).
With no arguments, Chain returns a no-op middleware.
func DiscardLogging ¶
func DiscardLogging() Middleware
DiscardLogging returns middleware that tracks request timing but writes no log output. Useful for benchmarks and high-throughput production paths where structured logging is handled externally (e.g. by a reverse proxy or APM agent).
func Idempotency ¶
func Idempotency(cfg IdempotencyConfig) Middleware
Idempotency returns Middleware that honours the Idempotency-Key header on configured methods. See IdempotencyConfig for tuning.
On a replay the middleware writes the cached status, headers, and body verbatim and adds Idempotent-Replay: true so the client can distinguish a replay from a fresh result.
func Logging ¶
func Logging() Middleware
Logging returns middleware that logs each request using slog.Default() at request time (not at construction). Kept as a convenience for code that doesn't have a logger to inject; new framework code should wire LoggingFn with an explicit logger source.
func LoggingFn ¶
func LoggingFn(getLogger func() *slog.Logger) Middleware
LoggingFn returns middleware that logs each request via the *slog.Logger returned by getLogger. getLogger is called per request so the upstream (e.g. framework.App) can hand out a logger that was swapped after the middleware was attached — this is how plugins can replace the logger after the chain is already wired.
If getLogger is nil or returns nil, slog.Default() is used.
func LoggingWithWriter ¶
func LoggingWithWriter(w io.Writer) Middleware
LoggingWithWriter returns logging middleware that writes to w as structured JSON. If w is nil, slog.Default() is used at request time.
Retained for tests and ad-hoc tooling that wants a fixed-destination logger; new framework code should prefer LoggingFn.
func MetricsMiddleware ¶
func MetricsMiddleware(m *Metrics) Middleware
MetricsMiddleware returns Middleware that records counters + latency for every served request. Route label uses r.Pattern when available (Go 1.22+ ServeMux fills it in), falling back to the URL path so unmatched paths don't explode cardinality with raw user input.
func RateLimit ¶
func RateLimit(cfg RateLimitConfig) Middleware
RateLimit returns Middleware that enforces a token-bucket rate limit per extracted key. Each key gets its own bucket; expired buckets eventually get reaped (every 5 minutes of inactivity).
Usage:
r.Use(middleware.RateLimit(middleware.RateLimitConfig{
Capacity: 30,
RefillEvery: time.Second,
RefillBy: 1,
}))
or via the default:
r.Use(middleware.RateLimit(middleware.RateLimitConfig{})) // 60/min/IP
func Recovery ¶
func Recovery() Middleware
Recovery returns middleware that catches panics in downstream handlers using slog.Default at request time. Kept as a convenience for code without a logger to inject; new framework code should prefer RecoveryFn with an explicit logger source.
func RecoveryFn ¶
func RecoveryFn(getLogger func() *slog.Logger) Middleware
RecoveryFn returns recovery middleware that logs panics via the *slog.Logger returned by getLogger. The accessor is called per request so a downstream `app.SetLogger` swap takes effect without rewiring the middleware chain.
If getLogger is nil or returns nil, slog.Default() is used.
The panic value, stack trace, and URL.Path are truncated to reasonable caps (4 KiB / 64 KiB / 2 KiB) so an attacker (or a buggy handler) can't drive multi-MB log entries through this middleware.
func RequestID ¶
func RequestID() Middleware
RequestID returns middleware that assigns a unique ID to each request. If an X-Request-ID header is present, well-formed (≤MaxRequestIDLen, alphanumeric / dot / dash / underscore), it is reused. Otherwise a new UUID v4 is generated.
Validation defends against: (1) huge headers amplified across every log entry, (2) header-reflection into response, (3) control chars (already blocked by net/http) — but our charset is stricter.
func SampledLogging ¶
func SampledLogging(sampleN int, slowThreshold time.Duration) Middleware
SampledLogging returns middleware that logs only a fraction of requests. It ALWAYS logs requests that are slow (> slowThreshold) or errored (status >= 400). Otherwise it logs 1-in-every-sampleN requests.
This addresses the ~200× overhead benchmark where Logging() dominates the default middleware chain cost. Use this as the default in production; switch to Logging() in dev or when debugging.
When sampleN is 0 or 1, every request is logged (equivalent to Logging()).
SampledLogging uses slog.Default(); use SampledLoggingFn to inject a specific logger source the same way LoggingFn does.
func SampledLoggingFn ¶
func SampledLoggingFn(sampleN int, slowThreshold time.Duration, getLogger func() *slog.Logger) Middleware
SampledLoggingFn is the injected-logger variant of SampledLogging. getLogger is called per logged event; nil or nil-returning getLogger falls back to slog.Default().
func SecurityHeaders ¶
func SecurityHeaders(cfg SecurityHeadersConfig) Middleware
SecurityHeaders adds conservative browser security headers.
The default Content-Security-Policy is strict (default-src 'self', no 'unsafe-inline'). The framework's UI host renders pages with all CSS and scripts as external resources under /__gofastr/* so they comply out of the box. img-src additionally allows data: so embedded data-URI images (icons, base64 placeholders) work.
func Timeout ¶
func Timeout(d time.Duration) Middleware
Timeout returns middleware that enforces a deadline on request processing. If the downstream handler does not complete within the given duration, a 504 Gateway Timeout response is returned.
The handler runs in a goroutine; a buffered response writer prevents concurrent writes to the underlying http.Header map between the handler goroutine and the timeout path.
func Tracing ¶
func Tracing() Middleware
Tracing returns Middleware that opens an OpenTelemetry span around every request. Span name is "HTTP {method} {route}" where route is the matched pattern (r.Pattern from Go 1.22+ ServeMux). Attributes recorded:
http.method — request method http.route — the route pattern (fallback "unmatched") http.status_code — final response status http.target — request URL path
Distributed-trace context is extracted from W3C traceparent / tracestate headers so spans from upstream services chain correctly. Outgoing responses get the corresponding headers injected if a propagator is configured.
Without a configured TracerProvider (the otel default), this middleware is essentially a no-op — spans are created against a no-op tracer that drops everything. Callers wire up Jaeger / OTLP / etc. via the standard otel.SetTracerProvider.
type Pipeline ¶
type Pipeline struct {
// contains filtered or unexported fields
}
Pipeline is an ordered list of middleware with a final handler. Use Build to produce a single http.Handler that runs all middleware in sequence around the final handler.
func NewPipeline ¶
func NewPipeline(mw ...Middleware) *Pipeline
NewPipeline creates a Pipeline with the given middleware.
func (*Pipeline) Build ¶
Build composes all pipeline middleware around the final handler and returns a single http.Handler.
Build(h) is equivalent to Chain(p.middleware...)(h).
func (*Pipeline) Use ¶
func (p *Pipeline) Use(mw ...Middleware) *Pipeline
Use appends middleware to the pipeline.
type RateLimitConfig ¶
type RateLimitConfig struct {
KeyFunc func(*http.Request) string
Capacity int
RefillEvery time.Duration
RefillBy int
StatusCode int
ErrorMessage string
// TrustProxyHeaders enables reading the client IP from the
// leftmost X-Forwarded-For (or X-Real-IP) entry. Only set this
// when the origin is behind a reverse proxy you control that
// rewrites or appends the header — otherwise an attacker can
// trivially defeat per-IP limiting by sending random XFF values.
//
// SECURITY: TrustProxyHeaders alone is NOT sufficient. The
// middleware will only trust the header when r.RemoteAddr (the
// immediate TCP peer) is one of TrustedProxies — see below.
// Without a trusted-proxy whitelist, XFF/X-Real-IP are ignored
// and the key falls back to r.RemoteAddr, so an attacker sending
// rotating header values from the same source can't get fresh
// buckets per request.
TrustProxyHeaders bool
// TrustedProxies is the set of TCP peer addresses (r.RemoteAddr,
// port stripped) whose X-Forwarded-For / X-Real-IP headers are
// trusted when TrustProxyHeaders is true. CIDR notation is
// accepted; bare IPs are matched exactly. If empty, no proxy is
// trusted and the header values are ignored (the key falls back
// to r.RemoteAddr).
TrustedProxies []string
}
RateLimitConfig controls the in-memory token-bucket rate limiter.
KeyFunc selects the per-bucket identity (per IP, per session, per API key, etc.). When KeyFunc is nil the default extractor uses r.RemoteAddr — X-Forwarded-For is *ignored* unless TrustProxyHeaders is set, because a caller in front of the origin can spoof XFF freely and would otherwise get a fresh bucket per request.
Capacity is the maximum number of tokens a bucket can hold (= peak burst allowed). RefillEvery / RefillBy together define the steady-state rate: "RefillBy tokens are added every RefillEvery". Defaults: 60 tokens, +60 every minute (i.e., 1 req/sec sustained with a 60-req burst).
When a request is rate-limited the handler responds 429 with a Retry-After header indicating seconds until the bucket would have one free token.
type SQLIdempotencyOption ¶
type SQLIdempotencyOption func(*SQLIdempotencyStore)
SQLIdempotencyOption configures the SQL store.
func WithSQLIdempotencyDialect ¶
func WithSQLIdempotencyDialect(dialect string) SQLIdempotencyOption
WithSQLIdempotencyDialect pins the SQL dialect ("postgres" or "sqlite") instead of running the auto-detection probe.
func WithSQLIdempotencyInFlightTTL ¶
func WithSQLIdempotencyInFlightTTL(d time.Duration) SQLIdempotencyOption
WithSQLIdempotencyInFlightTTL overrides the default 30s in-flight claim TTL. Set this above the worst-case handler latency: a slower handler whose claim expires mid-execution lets retries see "no row" and execute again.
func WithSQLIdempotencyTTL ¶
func WithSQLIdempotencyTTL(d time.Duration) SQLIdempotencyOption
WithSQLIdempotencyTTL overrides the default 24h cached-response TTL.
func WithSQLIdempotencyTable ¶
func WithSQLIdempotencyTable(name string) SQLIdempotencyOption
WithSQLIdempotencyTable overrides the default "idempotency_keys" table name.
type SQLIdempotencyStore ¶
type SQLIdempotencyStore struct {
// contains filtered or unexported fields
}
SQLIdempotencyStore persists idempotency claims to a SQL database. It works with sqlite and postgres; dialect is pinned via WithSQLIdempotencyDialect or auto-detected via SELECT version() at construction.
The table is created on first use with the supplied (or default) name. Records older than TTL are removed lazily on Begin — at most once per minute per store instance, not on every request.
Schema (all dialects):
idempotency_keys(
key TEXT PRIMARY KEY,
fingerprint TEXT NOT NULL,
status INTEGER, -- nullable while in-flight
headers TEXT, -- JSON, http.Header shape
body BLOB,
expires_at TIMESTAMP/TIMESTAMPTZ NOT NULL,
created_at TIMESTAMP/TIMESTAMPTZ NOT NULL
)
func NewSQLIdempotencyStore ¶
func NewSQLIdempotencyStore(db *sql.DB, opts ...SQLIdempotencyOption) (*SQLIdempotencyStore, error)
NewSQLIdempotencyStore constructs a SQL-backed IdempotencyStore and ensures the backing table exists.
func (*SQLIdempotencyStore) Begin ¶
func (s *SQLIdempotencyStore) Begin(ctx context.Context, key, fingerprint string) (*IdempotentResponse, bool, error)
Begin implements IdempotencyStore. The implementation is robust to concurrent inserts: an `INSERT … ON CONFLICT DO NOTHING` either claims the row (RowsAffected=1) or loses the race (RowsAffected=0), in which case we re-read and report the winner's state instead of surfacing a PK-violation error that would otherwise look like a generic store failure and bypass idempotency.
func (*SQLIdempotencyStore) Finish ¶
func (s *SQLIdempotencyStore) Finish(ctx context.Context, key string, resp *IdempotentResponse) error
Finish implements IdempotencyStore.
type SecurityHeadersConfig ¶
type SecurityHeadersConfig struct {
ContentSecurityPolicy string
ReferrerPolicy string
FrameOptions string
PermissionsPolicy string
// CrossOriginResourcePolicy controls the Cross-Origin-Resource-Policy
// header. Defaults to "same-origin" — Spectre-style cross-origin
// reads of this resource are blocked. Set to "cross-origin" to opt
// out for CDN-style assets.
CrossOriginResourcePolicy string
// CrossOriginOpenerPolicy controls the Cross-Origin-Opener-Policy
// header. Defaults to "same-origin" — the document gets a fresh
// browsing context group, blocking cross-origin window references
// and the broader XS-Leaks class. Set to "unsafe-none" to opt out
// when you intentionally interact with cross-origin windows.
CrossOriginOpenerPolicy string
// HSTS enables the Strict-Transport-Security header.
// When set (non-zero), browsers will only use HTTPS for this duration.
// Requires HTTPS to be active; the header is silently skipped on plain HTTP.
// Recommended: 31536000 seconds (1 year) for production.
// Only takes effect when Secure is true.
HSTSMaxAge int
HSTSIncludeSub bool
HSTSPreload bool
// Secure indicates whether the connection is over HTTPS.
// When true AND HSTSMaxAge > 0, the Strict-Transport-Security header is added.
// Defaults to true.
Secure bool
}
SecurityHeadersConfig controls defensive HTTP response headers.