ratelimit

package
v0.8.0 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package ratelimit is a reusable, host-composable request rate limiter, independent of auth, admin, accounts, and realtime. It never imports any of those packages: a host supplies its own key extraction (see KeyFunc), so ratelimit stays usable for plain IP-based limiting, identity-aware limiting on top of a host's own auth, or anything else reachable from an *http.Request.

This is deliberately not a replacement for admin/accounts' own failed-login-attempt limiter (internal/security.RateLimiter): that one counts authentication failures in a sliding window, this one counts every request via a token bucket. The semantics differ on purpose and are not merged.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Middleware

func Middleware(limiter *Limiter, key KeyFunc, opts ...MiddlewareOption) tango.Middleware

Middleware wraps next as ordinary tango.Middleware: composes at any of tanGO's existing global/group/route attachment tiers, and applies equally to a WebSocket upgrade route, since an upgrade is just a specially-shaped GET request hitting the same routing layer.

key(r) is called first. A non-nil error, or an empty key with a nil error, both go to errorHandler — never treated as a rejected Decision. Otherwise limiter.Take is called; on Decision.Allowed == false, limitedHandler runs instead of next.

Types

type Decision

type Decision struct {
	Allowed    bool
	Limit      int
	Remaining  int
	RetryAfter time.Duration
}

Decision is the outcome of one Limiter.Take call — enough information for middleware, or any custom response, to build rate-limit headers or a body without reaching into Limiter's internal state (which stays unexported).

type ErrorHandler

type ErrorHandler func(http.ResponseWriter, *http.Request, error)

ErrorHandler responds when KeyFunc itself fails — distinct from a rejected Decision, so a key-extraction failure is never treated as (and never looks like, in logs or the response) "this caller is over budget."

type KeyFunc

type KeyFunc func(*http.Request) (string, error)

KeyFunc extracts a rate-limit key from a request. Caller-supplied: ratelimit never imports auth, auth/jwt, or accounts. A host may key on remote IP (see RemoteIPKey), a JWT claim, session identity, route identity, tenant identity, or anything else reachable from *http.Request.

func RemoteIPKey

func RemoteIPKey(trustedProxies ...*net.IPNet) KeyFunc

RemoteIPKey returns a KeyFunc keyed on the request's client IP.

With no trustedProxies given, it always uses r.RemoteAddr's host (via net.SplitHostPort, falling back to the raw RemoteAddr value if it doesn't parse as host:port); forwarded headers are never consulted. This matches admin/accounts' login rate limiter when it is not configured with trusted proxies.

With trustedProxies given, and only when the immediate peer (r.RemoteAddr's host) is an IP inside one of the given networks, X-Forwarded-For is read from the right: addresses that are themselves in trustedProxies are skipped, and the first other address, the one your nearest trusted proxy saw, is the key. Whatever the client wrote to the left of it is ignored, so a client cannot pick its own key even through a proxy that appends to the header. An entry that is not a bare IP address, or a list with no usable entry, falls back to the peer's address. A valid address is returned in canonical form. X-Real-IP is never read. From an untrusted peer, headers are ignored even if present.

type LimitedHandler

type LimitedHandler func(http.ResponseWriter, *http.Request, Decision)

LimitedHandler responds to a request Limiter.Take rejected. It receives the computed Decision so a host can build its own headers/body from it (e.g. X-RateLimit-Remaining) instead of trusting the default response.

type Limiter

type Limiter struct {
	// contains filtered or unexported fields
}

Limiter is a concrete, in-memory, per-process token-bucket rate limiter. There is no storage interface in this package: a correct distributed token bucket needs one atomic check-and-consume operation, not a generic Get/Set store, and that seam is deferred until a real second backend exists to prove its shape.

func NewLimiter

func NewLimiter(opts Options) (*Limiter, error)

NewLimiter constructs a Limiter. Limit and Refill must be positive; Clock defaults to time.Now when nil.

func (*Limiter) Take

func (l *Limiter) Take(ctx context.Context, key string, now time.Time, cost int) (Decision, error)

Take consumes cost tokens for key at time now, returning the resulting Decision. cost must be positive — a caller charging more than 1 token per request (e.g. for an expensive operation) passes a larger cost, but cost <= 0 is a caller error, never silently treated as 1. cost must also not exceed Limit: a bucket's capacity never holds more than Limit tokens, so a request costing more than that could never succeed no matter how long it waited — that's a caller/configuration error, not an ordinary rejection with a finite RetryAfter.

A key seen for the first time starts with a full bucket (Limit tokens), so the very first requests for any key may burst up to Limit before being throttled.

type MiddlewareOption

type MiddlewareOption func(*middlewareConfig)

MiddlewareOption configures Middleware.

func WithCost

func WithCost(cost int) MiddlewareOption

WithCost sets how many tokens one request consumes. Defaults to 1.

func WithErrorHandler

func WithErrorHandler(h ErrorHandler) MiddlewareOption

WithErrorHandler overrides the default 400 JSON response for a KeyFunc failure.

func WithLimitedHandler

func WithLimitedHandler(h LimitedHandler) MiddlewareOption

WithLimitedHandler overrides the default 429 JSON response for a rejected request.

type Options

type Options struct {
	// Limit is the bucket capacity — the maximum burst of tokens a key can
	// accumulate and spend at once. Must be positive.
	Limit int
	// Refill is the time it takes to accumulate one additional token.
	// Must be positive.
	Refill time.Duration
	// Clock returns the current time; it defaults to time.Now. Tests
	// inject a fake clock here so token-bucket refill/burst/exhaustion
	// behavior never depends on a real-time sleep. It is used internally
	// by Middleware (see middleware.go) — Limiter.Take itself always takes
	// an explicit now, for callers (and tests) that want full manual
	// control without going through Options at all.
	Clock func() time.Time
}

Options configures a Limiter's token-bucket shape and clock.

Jump to

Keyboard shortcuts

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