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
- type Config
- type Limiter
- 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) 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 that strips client-supplied XFF). The default (trustXFF=false) ignores XFF — otherwise a single curl with a rotating X-Forwarded-For header bypasses every per-IP limit.
Types ¶
type Config ¶
type Config struct {
MaxAttempts int
Window time.Duration
BlockDuration time.Duration
TrustForwardedFor bool
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
}
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 leftmost X-Forwarded-For entry is used as the client IP by the default (IP-keyed) middleware. ONLY enable this if the server sits behind a trusted reverse proxy that strips client-supplied XFF headers — otherwise an attacker rotates the header per request and bypasses every per-IP limit. Default is false (use the connection's RemoteAddr).
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.
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) 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) 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.