Documentation
¶
Overview ¶
Package ratelimit is the framework's general-purpose HTTP rate limiter.
It enforces a per-key sliding-window policy: up to Config.MaxAttempts requests are admitted within Config.Window; the next request triggers a hard block lasting Config.BlockDuration during which every request for that key gets 429. This is the "N actions per period, then lockout" shape, the right tool for brute-force surfaces, write-heavy endpoints, and any route where a steady refill rate (the token-bucket model) is the wrong abstraction.
Sliding window vs. token bucket ¶
GoFastr ships two limiters with deliberately different semantics:
- framework/ratelimit (this package): sliding window + lockout. Use it when you want "at most N per period, then block". The auth battery builds its login / register / password-reset limiters on top of it.
- core/middleware.RateLimit: token bucket. Use it for steady API throughput ("1 req/s sustained, burst of 60") and when you want the RateLimit-* budget headers so well-behaved clients can self-pace.
Per-replica, not distributed ¶
With a nil Config.Store the window is held in process memory, so the budget is per-replica: N replicas each allow MaxAttempts. For a replica-wide (or fleet-wide) budget, supply a Store such as battery/auth.SQLRateLimitStore. See "Shared store" below.
Index ¶
- func ClientIP(r *http.Request, trustXFF bool) string
- func NamespaceScope(scope string) string
- type Config
- type Limiter
- func (rl *Limiter) Admit(ctx context.Context, key string) (allowed bool, retryAfter time.Duration, err error)
- func (rl *Limiter) Allow(key string) (allowed bool, retryAfter time.Duration)
- func (rl *Limiter) AllowContext(ctx context.Context, key string) (allowed bool, retryAfter time.Duration)
- func (rl *Limiter) ClientIP(r *http.Request) string
- func (rl *Limiter) Middleware() func(http.Handler) http.Handler
- func (rl *Limiter) MiddlewareByKey(keyFunc func(*http.Request) string) func(http.Handler) http.Handler
- type Store
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ClientIP ¶
ClientIP extracts the request IP. It honours X-Forwarded-For only when trustXFF is true (typically behind a trusted reverse proxy). The default (trustXFF=false) ignores XFF, otherwise a single curl with a rotating X-Forwarded-For header bypasses every per-IP limit.
With trustXFF the immediate peer is taken to be the one proxy and the rightmost X-Forwarded-For entry, the address that proxy observed, is the client. Entries to its left are client-supplied behind an appending proxy and are never used. A deployment with more than one proxy tier sets Config.TrustedProxies and calls Limiter.ClientIP instead.
func NamespaceScope ¶ added in v0.87.0
Config controls a per-key sliding-window limiter.
MaxAttempts requests are permitted within Window. The MaxAttempts+1th request triggers a block of BlockDuration during which every request for that key gets 429.
Defaults (filled in by NewLimiter when zero): MaxAttempts=10, Window=15m, BlockDuration=30m.
TrustForwardedFor: when true, the default (IP-keyed) middleware reads the client IP from X-Forwarded-For. ONLY enable this if the server sits behind a reverse proxy you control, otherwise an attacker rotates the header per request and bypasses every per-IP limit. Default is false (use the connection's RemoteAddr). The header is read from the RIGHT: entries left of the hop your proxy wrote are client-supplied when the proxy appends, so the key is the rightmost hop that is not in TrustedProxies.
TrustedProxies lists your own proxy tiers (IPs or CIDRs, CDN ranges included). When set, only a request whose TCP peer is in the list may speak for X-Forwarded-For, and listed hops are skipped on the walk. When empty, the immediate peer is taken to be your one proxy and the rightmost entry is the client; behind more than one tier that entry is your inner proxy, so list the tiers.
Store: when non-nil, attempts are recorded in the shared backend instead of process memory, so the budget holds across replicas: MaxAttempts total, not MaxAttempts × N, and a block on one replica blocks on all. On a store error the limiter fails CLOSED (denies), an attacker must never be able to lift the limit by degrading its backend. One store instance can back several limiters: keys are namespaced by Scope.
NamespaceScope encodes delimiter bytes in a Store scope so the scope/key boundary stays unambiguous even when a host or caller includes "|" or "%".
Types ¶
type Config ¶
type Config struct {
MaxAttempts int
Window time.Duration
BlockDuration time.Duration
TrustForwardedFor bool
TrustedProxies []string
Store Store
Scope string
// DevMode relaxes the limiter: when true, AllowContext short-circuits and
// admits every attempt without touching either backend. Intended ONLY for
// non-production deploys so local tooling that hammers an endpoint from one
// IP (localhost) is never locked out. The default (false) keeps the limiter
// fail-closed, production must NEVER set this.
DevMode bool
}
Scope namespaces this limiter's keys inside a shared Store. Set it explicitly when several limiters share one Store so their keys never collide. Ignored when Store is nil.
type Limiter ¶
type Limiter struct {
// contains filtered or unexported fields
}
Limiter is a sliding-window rate limiter keyed by an arbitrary string (typically the client IP). The zero value is not usable, construct one with NewLimiter.
func NewLimiter ¶
NewLimiter constructs a Limiter with the given config. Zero fields fall back to the documented defaults.
func (*Limiter) Admit ¶ added in v0.87.0
func (rl *Limiter) Admit(ctx context.Context, key string) (allowed bool, retryAfter time.Duration, err error)
Admit is AllowContext that also returns the shared store's error. A store failure still denies (allowed is false, retryAfter the short outage hint). The error lets a caller that acts on a spent budget, such as ending a session, tell an outage from a budget used up.
func (*Limiter) Allow ¶
Allow records an attempt for key and returns whether it is allowed. If not allowed, retryAfter is the duration the caller should communicate in a Retry-After header. Equivalent to AllowContext with a background context, HTTP paths should prefer AllowContext(r.Context(), key) so a shared store can observe request cancellation.
func (*Limiter) AllowContext ¶
func (rl *Limiter) AllowContext(ctx context.Context, key string) (allowed bool, retryAfter time.Duration)
AllowContext records an attempt for key against the configured backend: the shared Store when one is set (replica-wide budget), the in-process sliding window otherwise. A store failure DENIES the attempt, the limiter guards brute-force surfaces, so it must fail closed: degrading its backend must never lift the limit.
DevMode (see Config) is an explicit, tested short-circuit: when set, every attempt is admitted without touching either backend. This is the dev-only relaxation that stops local tooling being locked out; production never sets it, so the fail-closed guarantee holds.
func (*Limiter) ClientIP ¶ added in v0.87.0
ClientIP is the package ClientIP under this limiter's TrustForwardedFor and TrustedProxies: with a TrustedProxies list, only a listed peer may speak for X-Forwarded-For and listed hops are skipped from the right.
func (*Limiter) Middleware ¶
Middleware returns an HTTP middleware that rate-limits by client IP (the default key). Blocked requests get 429 with a Retry-After header.
It emits ONLY Retry-After and never the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset budget headers that the token-bucket middleware (core/middleware.RateLimit) exposes: a live remaining-attempt count on a lockout-style limiter would hand an attacker exact brute-force pacing information on security-sensitive routes, and adds nothing useful on non-security routes where Retry-After already lets a client back off. For budget headers and a refill-rate model, use core/middleware.RateLimit.
To group by something other than IP (API key, user id, route param), use MiddlewareByKey.
func (*Limiter) MiddlewareByKey ¶
func (rl *Limiter) MiddlewareByKey(keyFunc func(*http.Request) string) func(http.Handler) http.Handler
MiddlewareByKey returns an HTTP middleware that rate-limits using keyFunc to derive the per-request identity. A nil keyFunc falls back to the client IP. Blocked requests get 429 with a Retry-After header; see Middleware for why the budget headers are intentionally omitted.
type Store ¶
type Store interface {
Allow(ctx context.Context, key string, cfg Config) (allowed bool, retryAfter time.Duration, err error)
}
Store is the optional shared backend for a Limiter. When Config.Store is set, every replica consults the same attempt ledger, so the budget stays MaxAttempts total instead of MaxAttempts × replicas and a block on one replica holds on all of them.
battery/auth.SQLRateLimitStore implements this over SQLite or PostgreSQL and is the reference implementation; a custom Redis/etcd backend only needs to satisfy this interface. The implementation must derive per-limiter state from the namespaced key alone, it receives the full Config but should treat Scope + key as the identity.
AllowContext calls the store outside the limiter's mutex, so concurrent callers on one or many replicas reach Allow together. The admission decision must be atomic per key: a concurrent burst admits at most MaxAttempts. Counting and then recording in separate operations breaks that, since every caller reads the same pre-insert count.