Documentation
¶
Overview ¶
Package ratelimit provides HTTP rate limiting middleware with support for per-principal and per-application rate limits.
The package supports multiple storage backends (in-memory, Redis) and integrates with Cedar policies for dynamic tier-based rate limiting.
Index ¶
- Constants
- Variables
- func Anonymous(r *http.Request) bool
- func ForwardedClientIP(trustedProxies ...string) (func(*http.Request) string, error)deprecated
- func NewRedisClient(cfg *RedisConfig) redis.UniversalClient
- func PeekForm(r *http.Request) url.Values
- func PeerIP(r *http.Request) string
- func PolicyApplied(ctx context.Context) bool
- func RequestClientID(r *http.Request) string
- func RetryAfterSeconds(d time.Duration) int
- func WriteHeaders(h http.Header, name string, limit Limit, res Result)
- func WriteTooManyRequests(w http.ResponseWriter, wait time.Duration)
- type CoreAPIResolver
- type CoreAPIResolverOption
- type Decision
- type KeyFunc
- func AddressKey(res *clientaddr.Resolver) KeyFunc
- func ClientEndpointKey() KeyFunc
- func ClientKey() KeyFunc
- func CompositeKey() KeyFunc
- func EndpointKey(inner KeyFunc) KeyFunc
- func ForwardedIPKey(trustedProxies ...string) (KeyFunc, error)deprecated
- func IPKey() KeyFunc
- func PrincipalEndpointKey() KeyFunc
- func PrincipalKey() KeyFunc
- func SignedInKey() KeyFunc
- type KeyKind
- type Limit
- type LimitResolver
- type Limiter
- type MemoryOption
- type MemoryStorage
- type Option
- func WithFailOpen() Option
- func WithKeyFunc(keyFunc KeyFunc) Option
- func WithLimit(limit Limit) Option
- func WithLogger(logger *slog.Logger) Option
- func WithName(name string) Option
- func WithObservability(obs *observability.Observability) Option
- func WithResolver(resolver LimitResolver) Option
- func WithSkip(skip func(r *http.Request) bool) Option
- type Peeker
- type Policy
- type PolicyConfig
- type RedisConfig
- type RedisOption
- type RedisStorage
- type Result
- type Route
- type RouteLimit
- type StaticResolver
- type Storage
- type TieredResolver
Constants ¶
const ( HeaderRateLimitPolicy = "RateLimit-Policy" HeaderRateLimit = "RateLimit" HeaderRetryAfter = "Retry-After" HeaderXRateLimitLimit = "X-RateLimit-Limit" HeaderXRateLimitRemaining = "X-RateLimit-Remaining" HeaderXRateLimitReset = "X-RateLimit-Reset" )
Response headers. RateLimit-Policy and RateLimit follow the IETF HTTPAPI draft "RateLimit header fields for HTTP"; the X-RateLimit-* headers are kept for clients written against them.
const ( RouteToken = "token" RouteLogin = "login" RouteSignup = "signup" RouteBFFAuth = "bff_auth" RouteHealth = "health" RouteStatic = "static" RoutePaymentWebhook = "payment_webhook" )
Names of the default route policies, for overriding them by name with MergeRoutes.
const DefaultMaxKeys = 100_000
DefaultMaxKeys is the number of keys MemoryStorage tracks by default.
const DefaultPolicyName = "default"
DefaultPolicyName names a Limiter's policy in RateLimit-Policy when WithName is not given.
Variables ¶
var ( // DefaultAddressLimit is the default layer's limit per client address: // generous, so that only floods reach it. DefaultAddressLimit = PerMinute(300) // DefaultSignedInLimit is the limit per signed-in principal or service // account, checked after authentication. DefaultSignedInLimit = PerMinute(1200) )
Default limits of the layers.
var ( ErrRateLimitExceeded = errors.New("rate limit exceeded") ErrStorageFailure = errors.New("rate limit storage failure") )
Common errors returned by the rate limiter.
var ErrClosed = errors.New("ratelimit: storage closed")
ErrClosed is returned by a closed storage.
var ErrTooManyKeys = errors.New("ratelimit: too many tracked keys")
ErrTooManyKeys is returned when a new key arrives while the storage is tracking its maximum number of live keys. Callers should treat it as a refusal: keys are attacker-influenced on pre-authentication endpoints, and admitting them without bound would exhaust memory.
Functions ¶
func Anonymous ¶
Anonymous reports whether a request has no signed-in caller in its context, for WithSkip.
func ForwardedClientIP
deprecated
added in
v0.14.0
ForwardedClientIP returns a function that finds the client's address behind reverse proxies. When the direct peer is one of the trusted proxy networks, it reads X-Forwarded-For from the right, skipping addresses that are themselves trusted proxies, and returns the first address a trusted proxy did not vouch for. Requests whose peer is not a trusted proxy, or whose header holds nothing usable, are attributed to the peer itself.
Deprecated: build a clientaddr.Resolver, which also reads single-value headers such as CF-Connecting-IP, and pass its ClientIP method, or key limits with AddressKey.
func NewRedisClient ¶
func NewRedisClient(cfg *RedisConfig) redis.UniversalClient
NewRedisClient creates a Redis client from configuration.
func PeekForm ¶
PeekForm returns the fields of a POST request's form body (application/x-www-form-urlencoded) without consuming it: the body is put back unchanged for the handler. Bodies over 64 KiB, other content types and unparseable bodies give no fields. When the form is already parsed it returns r.PostForm.
func PeerIP ¶ added in v0.14.0
PeerIP returns the IP address of the connection's direct peer, with the port removed and IPv4-mapped IPv6 addresses unmapped. It is the one address a client cannot choose, so it is what limits key on by default. It returns "" when the peer address cannot be parsed. It is clientaddr.PeerIP.
func PolicyApplied ¶
PolicyApplied reports whether a Policy has limited the request.
func RequestClientID ¶
RequestClientID returns the OAuth client a token request names: the HTTP Basic user name (form-decoded, RFC 6749 §2.3.1) or the client_id field of a form body. A body it reads is put back unchanged for the handler. It returns "" when the request names no client.
func RetryAfterSeconds ¶
RetryAfterSeconds is the Retry-After value for a refused request: the wait rounded up to whole seconds, and at least one, so a client never retries at once.
func WriteHeaders ¶
WriteHeaders adds the rate limit headers for one checked limit: an item in RateLimit-Policy (quota q and window w in seconds) and in RateLimit (remaining r and seconds to reset t), and sets X-RateLimit-Limit, -Remaining and -Reset. Several limits checked for one request each add their item.
func WriteTooManyRequests ¶
func WriteTooManyRequests(w http.ResponseWriter, wait time.Duration)
WriteTooManyRequests answers 429 with Retry-After set to wait (rounded up, at least one second).
Types ¶
type CoreAPIResolver ¶
type CoreAPIResolver struct {
// contains filtered or unexported fields
}
CoreAPIResolver adapts coreapi.PolicyStore to the LimitResolver interface. It looks up rate limit policies based on the OAuth client ID from JWT claims.
func NewCoreAPIResolver ¶
func NewCoreAPIResolver(store coreapi.PolicyStore, opts ...CoreAPIResolverOption) *CoreAPIResolver
NewCoreAPIResolver creates a LimitResolver that uses coreapi.PolicyStore.
func (*CoreAPIResolver) GetPolicyForRequest ¶
func (r *CoreAPIResolver) GetPolicyForRequest(ctx context.Context, req *http.Request) *coreapi.RateLimitPolicy
GetPolicyForRequest returns the policy that would be applied to a request. Useful for debugging and displaying policy info in responses.
func (*CoreAPIResolver) InvalidateCache ¶
func (r *CoreAPIResolver) InvalidateCache(clientID string)
InvalidateCache clears the policy cache.
type CoreAPIResolverOption ¶
type CoreAPIResolverOption func(*CoreAPIResolver)
CoreAPIResolverOption configures a CoreAPIResolver.
func WithResolverCache ¶
func WithResolverCache(ttl time.Duration) CoreAPIResolverOption
WithResolverCache enables caching of policy lookups.
type Decision ¶
type Decision int
Decision is the outcome of Check.
const ( // Allowed: the request is within the limit. Allowed Decision = iota // Refused: the limit is used up; the response was written. Refused // response was written. Unavailable )
Outcomes of Check.
func Check ¶
func Check(w http.ResponseWriter, r *http.Request, storage Storage, name, key string, limit Limit, failOpen bool, logger *slog.Logger) Decision
Check checks one limit inside a handler, on the storage the layers share, for limits that need the request's outcome or a key only the handler knows (an account, a service account). It writes the rate limit headers, and on refusal the 429 or 503 response; with failOpen a storage failure is logged and allowed. name names the policy in the headers and is the key's namespace.
type KeyFunc ¶
KeyFunc extracts the rate limit key from a request. Common implementations include extracting principal ID or client ID from JWT.
func AddressKey ¶
func AddressKey(res *clientaddr.Resolver) KeyFunc
AddressKey returns a KeyFunc that keys on the client address res finds (see clientaddr). A nil res keys on the direct peer, as IPKey does. An unparseable peer address shares one bucket rather than escaping limits.
func ClientEndpointKey ¶
func ClientEndpointKey() KeyFunc
ClientEndpointKey is a convenience function combining ClientKey with EndpointKey.
func ClientKey ¶
func ClientKey() KeyFunc
ClientKey returns a KeyFunc that extracts the OAuth client ID (azp) from JWT claims. Falls back to IP address if no client ID is present.
func CompositeKey ¶
func CompositeKey() KeyFunc
CompositeKey returns a KeyFunc that combines principal and client IDs. Format: "pid:{principal_id}:client:{client_id}" or fallback to IP.
func EndpointKey ¶
EndpointKey wraps another KeyFunc to add endpoint-specific rate limiting. Format: "{inner_key}:path:{path}:method:{method}"
func ForwardedIPKey
deprecated
added in
v0.14.0
ForwardedIPKey is IPKey for a server behind reverse proxies: it takes the client address from X-Forwarded-For when the request arrives from one of the trusted proxy networks (see ForwardedClientIP).
Deprecated: use AddressKey with a clientaddr.Resolver, the client-address rule shared with SystemAuth and the BFF.
func IPKey ¶
func IPKey() KeyFunc
IPKey returns a KeyFunc that keys on the IP address of the direct peer (see PeerIP). Behind a reverse proxy every client would share the proxy's address; use ForwardedIPKey there. Forwarding headers are never believed unless a trusted proxy is configured, because any client can send them.
func PrincipalEndpointKey ¶
func PrincipalEndpointKey() KeyFunc
PrincipalEndpointKey is a convenience function combining PrincipalKey with EndpointKey.
func PrincipalKey ¶
func PrincipalKey() KeyFunc
PrincipalKey returns a KeyFunc that extracts the principal ID from JWT claims. Falls back to IP address if no principal is authenticated.
func SignedInKey ¶
func SignedInKey() KeyFunc
SignedInKey keys on the signed-in caller: the principal (person or service account) requestprincipal or the JWT middleware put in the context. Requests without one get "", so pair it with WithSkip(Anonymous).
type Limit ¶
type Limit struct {
// Rate is the number of requests allowed per Period.
Rate int
// Period is the time window for the rate limit (e.g., time.Minute).
Period time.Duration
// Burst is the maximum number of requests allowed in a single burst.
// If zero, defaults to Rate.
Burst int
}
Limit defines rate limit parameters.
func DefaultLimit ¶
func DefaultLimit() Limit
DefaultLimit returns a sensible default rate limit (100 requests per minute).
type LimitResolver ¶
type LimitResolver interface {
// Resolve returns the rate limit for the given key and context.
// The key is typically derived from the principal ID or client ID.
Resolve(ctx context.Context, key string, r *http.Request) Limit
}
LimitResolver determines the rate limit for a given request. This allows dynamic rate limiting based on user tier, endpoint, etc.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter is the main rate limiter type.
func NewSignedInLimiter ¶
NewSignedInLimiter returns the limiter for signed-in callers, to install after authentication: keyed by principal or service account (SignedInKey), skipping anonymous requests, failing open, and named "signed_in". A zero limit uses DefaultSignedInLimit. opts are applied after these.
func (*Limiter) Middleware ¶
Middleware returns an HTTP middleware that enforces rate limits. Every checked response carries the RateLimit-Policy, RateLimit and X-RateLimit-* headers; a refused one is a 429 with Retry-After of at least one second. A storage failure answers 503 unless WithFailOpen is given, and a full MemoryStorage (ErrTooManyKeys) is always a 429.
type MemoryOption ¶
type MemoryOption func(*MemoryStorage)
MemoryOption configures MemoryStorage.
func WithCleanupInterval ¶
func WithCleanupInterval(d time.Duration) MemoryOption
WithCleanupInterval sets the interval for cleaning up expired windows.
func WithMaxKeys ¶ added in v0.14.0
func WithMaxKeys(n int) MemoryOption
WithMaxKeys sets the number of keys the storage tracks at once (default DefaultMaxKeys). Expired windows are discarded to make room first.
type MemoryStorage ¶
type MemoryStorage struct {
// contains filtered or unexported fields
}
MemoryStorage is an in-memory rate limit storage using sliding window algorithm. Suitable for single-instance deployments and development/testing.
func NewMemoryStorage ¶
func NewMemoryStorage(opts ...MemoryOption) *MemoryStorage
NewMemoryStorage creates a new in-memory rate limit storage.
type Option ¶
type Option func(*Limiter)
Option configures a Limiter.
func WithFailOpen ¶ added in v0.14.0
func WithFailOpen() Option
WithFailOpen lets requests through when the storage fails. By default the middleware fails closed, answering 503, because a storage outage otherwise switches off the limits that protect authentication endpoints at the moment an attacker may be causing it.
func WithKeyFunc ¶
WithKeyFunc sets the function to extract rate limit keys from requests.
func WithLogger ¶ added in v0.14.0
WithLogger sets the logger for storage failures.
func WithName ¶
WithName names the limit: the policy name in the RateLimit-Policy and RateLimit headers and in metrics. Default: DefaultPolicyName.
func WithObservability ¶
func WithObservability(obs *observability.Observability) Option
WithObservability sets the observability provider for metrics and tracing.
func WithResolver ¶
func WithResolver(resolver LimitResolver) Option
WithResolver sets a custom limit resolver.
type Peeker ¶
type Peeker interface {
// Peek returns what Allow would return, without counting.
Peek(ctx context.Context, key string, limit Limit) (Result, error)
}
Peeker is storage that can report a key's state without counting a request. MemoryStorage and RedisStorage implement it.
type Policy ¶
type Policy struct {
// contains filtered or unexported fields
}
Policy is the rate limiting a server applies before authentication: a default limit per client address on every route, and route policies for the credential endpoints. Install its Middleware outermost, before authentication; put a signed-in limit (NewSignedInLimiter) after it.
func (*Policy) Address ¶
func (p *Policy) Address() *clientaddr.Resolver
Address returns the client-address resolver.
func (*Policy) Middleware ¶
Middleware enforces the default layer and the route policies.
type PolicyConfig ¶
type PolicyConfig struct {
// Storage keeps the counters (required): MemoryStorage for one
// instance, RedisStorage when several share the limits.
Storage Storage
// Address finds the client address (see clientaddr). Nil uses the
// direct peer.
Address *clientaddr.Resolver
// Default is the default layer's limit per address. Zero uses
// DefaultAddressLimit; DisableDefault turns the layer off.
Default Limit
DisableDefault bool
// DefaultFailClosed answers 503 when the storage fails on the default
// layer. By default the default layer lets requests through and logs;
// the route limits of the credential endpoints still refuse.
DefaultFailClosed bool
// Routes is the route table. Nil uses DefaultRoutes; an empty non-nil
// table has no route policies.
Routes []Route
// ExemptNetworks are client networks, in CIDR notation or as single
// addresses, exempt from every limit: back-channel callers such as
// relying parties' servers.
ExemptNetworks []string
// KnownClient reports whether an OAuth client exists. With it, client
// limits give only registered clients their own bucket, and requests
// naming others share one, so made-up client IDs cannot grow the
// storage. Optional.
KnownClient func(ctx context.Context, clientID string) bool
// Logger receives storage failures. Default: slog.Default().
Logger *slog.Logger
// Observability records metrics. Optional.
Observability *observability.Observability
}
PolicyConfig configures a Policy.
type RedisConfig ¶
type RedisConfig struct {
// Addr is the Redis server address (e.g., "localhost:6379").
Addr string
// Password is the Redis password (optional).
Password string
// DB is the Redis database number (default 0).
DB int
// PoolSize is the maximum number of connections (default 10).
PoolSize int
// Cluster enables Redis Cluster mode.
Cluster bool
// ClusterAddrs are the cluster node addresses (used when Cluster is true).
ClusterAddrs []string
}
RedisConfig contains configuration for connecting to Redis.
type RedisOption ¶
type RedisOption func(*RedisStorage)
RedisOption configures RedisStorage.
func WithKeyPrefix ¶
func WithKeyPrefix(prefix string) RedisOption
WithKeyPrefix sets a prefix for all rate limit keys in Redis.
type RedisStorage ¶
type RedisStorage struct {
// contains filtered or unexported fields
}
RedisStorage is a Redis-backed rate limit storage using sliding window algorithm. Suitable for distributed deployments where multiple instances share rate limit state.
func NewRedisStorage ¶
func NewRedisStorage(client redis.UniversalClient, opts ...RedisOption) *RedisStorage
NewRedisStorage creates a new Redis-backed rate limit storage. The client can be *redis.Client, *redis.ClusterClient, or any UniversalClient.
func (*RedisStorage) Allow ¶
Allow implements Storage.Allow using a sliding window algorithm with Redis.
func (*RedisStorage) Close ¶
func (r *RedisStorage) Close() error
Close implements Storage.Close. Note: This does NOT close the Redis client since it may be shared.
type Result ¶
type Result struct {
// Allowed is true if the request should be permitted.
Allowed bool
// Remaining is the number of requests remaining in the current window.
Remaining int
// ResetAt is when the rate limit window resets.
ResetAt time.Time
// RetryAfter is the duration to wait before retrying (only set if not allowed).
RetryAfter time.Duration
}
Result contains the outcome of a rate limit check.
func Exhausted ¶
func Exhausted(ctx context.Context, storage Storage, name, key string, limit Limit) (Result, bool, error)
Exhausted reports whether the limit for key is used up, without counting a request when the storage can look without counting (Peeker). Storage that cannot look reports false. It is for limits that count only some outcomes, such as failed authentication: check Exhausted before the work, and Record after a counted outcome.
type Route ¶
type Route struct {
// Name names the policy in keys and in the RateLimit-Policy header.
Name string
// Paths are matched against the request path: "/oauth/token" matches
// that path only, "/login*" every path starting with "/login", and
// "/bff/auth/*" every path under "/bff/auth/" (and "/bff/auth"). When
// several routes match, the longest pattern wins.
Paths []string
// Methods restricts the route to these methods; empty matches all.
Methods []string
// Exempt skips the default layer and every route limit, for health
// checks, static assets, and callers whose addresses are shared, such
// as a payment provider's webhooks.
Exempt bool
// Limits are checked in order after the default layer.
Limits []RouteLimit
// FailOpen lets requests through when the storage fails. Routes fail
// closed by default: the credential endpoints refuse rather than run
// without limits.
FailOpen bool
}
Route is a route policy: the paths it covers, and either an exemption from address limits or limits checked after the default layer.
func DefaultRoutes ¶
func DefaultRoutes() []Route
DefaultRoutes is the default route table: the token endpoint limited per address and per client; login, sign-up and the BFF's authentication endpoints limited per address, lower than the default; health checks, static assets and the payment webhook exempt. Change entries by name with MergeRoutes.
func MergeRoutes ¶
MergeRoutes returns base with each override replacing the route of the same name, and overrides with new names appended. base is not modified.
type RouteLimit ¶
RouteLimit is one limit of a route policy and what it counts by.
type StaticResolver ¶
type StaticResolver struct {
Limit Limit
}
StaticResolver always returns the same limit.
type Storage ¶
type Storage interface {
// Allow checks if a request should be allowed and updates the counter.
// Returns the result of the check and any error.
Allow(ctx context.Context, key string, limit Limit) (Result, error)
// Reset clears the rate limit state for a key.
Reset(ctx context.Context, key string) error
// Close releases any resources held by the storage.
Close() error
}
Storage defines the interface for rate limit state storage.