go-chi/httprate

repository·master·Indexed 19 days ago

https://github.com/go-chi/httprate

A high-performance HTTP request rate limiter for Go implementing the Sliding Window Counter pattern. Designed as middleware for frameworks like chi, it supports rate limiting by client IP, URL path, or arbitrary keys. Features include IPv6 canonicalization, custom response handlers, response headers, and the LimitCounter interface for distributed backends like Redis.

Tokens
5.5K
Snippets
22
Records
29
Agent score
67%

What's inside httprate

  1. How httprate works and its core pattern

    master

    httprate is an HTTP request rate limiter based on the Sliding Window Counter pattern (inspired by Cloudflare). This pattern provides accurate, smooth traffic shaping and allows for easy distribution of rate limits across a cluster of servers.

    To coordinate rate limits across multiple microservices, you can implement the httprate.LimitCounter interface to support atomic increments and gets in a shared backend like Redis.

  2. Rate limit by client IP behind a proxy

    master

    When running behind a reverse proxy, load balancer, or CDN, the RemoteAddr is the proxy's IP, not the client's. To rate limit safely, you must first resolve a trusted client IP using chi middleware, then use that IP as the key in httprate.

    Security Warning: Do not use the deprecated LimitByRealIP or KeyByRealIP functions, as they are vulnerable to IP spoofing. Instead, use middleware.ClientIPFrom* to establish a trust model.

    Steps to implement:

    1. Resolve a trusted IP: Use exactly one middleware.ClientIPFrom* middleware that matches your deployment (e.g., ClientIPFromXFF for proxies with known IP ranges).
    2. Apply the limiter: Use httprate.LimitBy with a KeyFunc that reads the resolved IP via middleware.GetClientIP.
    3. Canonicalize IPv6: Use httprate.CanonicalizeIP to bucket IPv6 clients by their /64 prefix. This prevents clients from rotating through millions of IPv6 addresses to bypass limits.

    Deployment Mapping:

    SetupMiddleware to use
    Direct to internet (no proxy)middleware.ClientIPFromRemoteAddr
    Behind nginx, Cloudflare, or Apachemiddleware.ClientIPFromHeader("X-Real-IP") (or appropriate header)
    Behind trusted proxies with known rangesmiddleware.ClientIPFromXFF("10.0.0.0/8", ...)
    Behind a fixed number of dynamic proxiesmiddleware.ClientIPFromXFFTrustedProxies(n)
    import (
    	"github.com/go-chi/chi/v5/middleware"
    	"github.com/go-chi/httprate"
    )
    
    // 1. Resolve a trusted client IP.
    r.Use(middleware.ClientIPFromXFF("10.0.0.0/8"))
    
    // 2. Rate-limit by that trusted client IP.
    r.Use(httprate.LimitBy(100, time.Minute, clientIPKey))
    
    func clientIPKey(r *http.Request) (string, error) {
    	return httprate.CanonicalizeIP(middleware.GetClientIP(r.Context())), nil
    }
  3. Configure rate limit response headers

    master

    You can include rate limit metadata in your response headers using httprate.WithResponseHeaders. Pass a httprate.ResponseHeaders struct to define which headers to send.

    Available keys in httprate.ResponseHeaders:

    • Limit: The maximum number of requests allowed.
    • Remaining: The number of requests remaining in the current window.
    • Reset: The time until the window resets.
    • RetryAfter: The time to wait before retrying.
    • Increment: (Omit by leaving empty string to not send).

    To omit all rate limit headers, pass an empty httprate.ResponseHeaders{}.

    // Send custom headers
    r.Use(httprate.LimitBy(
    	1000,
    	time.Minute,
    	clientIPKey,
    	httprate.WithResponseHeaders(httprate.ResponseHeaders{
    		Limit:      "X-RateLimit-Limit",
    		Remaining:  "X-RateLimit-Remaining",
    		Reset:      "X-RateLimit-Reset",
    		RetryAfter: "Retry-After",
    		Increment:  "", // omit
    	}),
    ))
    
    // Omit response headers
    r.Use(httprate.LimitBy(
    	1000,
    	time.Minute,
    	clientIPKey,
    	httprate.WithResponseHeaders(httprate.ResponseHeaders{}),
    ))
  4. Customize error and rate-limit responses

    master

    You can override the default behavior when a request is rate-limited or when an internal error occurs by providing options during NewRateLimiter initialization:

    • Rate-limited response: Use an option to set onRateLimited. This is a http.HandlerFunc called when RespondOnLimit detects a limit breach.
    • Error response: Use an option to set onError. This is a function func(http.ResponseWriter, *http.Request, error) called when the LimitCounter returns an error. By default, it returns a 426 Precondition Required status.
  5. How to safely rate limit by client IP behind a proxy

    master

    The deprecated LimitByIP and LimitByRealIP functions are insecure or incorrect for production environments behind a reverse proxy (like Nginx, Cloudflare, or a Load Balancer).

    • LimitByIP (and KeyByIP) uses r.RemoteAddr, which will point to your proxy's IP, causing all users to share a single rate-limit bucket.
    • LimitByRealIP (and KeyByRealIP) trusts client-supplied headers like X-Forwarded-For, which can be easily spoofed by attackers to evade limits or perform Denial of Service attacks on other users.

    Recommended Secure Pattern: Use one of chi's middleware.ClientIPFrom* middlewares (available in chi v5.3.0+) to resolve the true client IP, then use httprate.LimitBy with httprate.CanonicalizeIP to bucket requests. CanonicalizeIP buckets IPv6 addresses by their /64 prefix.

    // 1. Install chi middleware to resolve the IP from a trusted source (e.g., XFF)
    r.Use(middleware.ClientIPFromXFF("10.0.0.0/8"))
    
    // 2. Use LimitBy with a key function that reads the resolved IP from context
    r.Use(httprate.LimitBy(100, time.Minute, func(r *http.Request) (string, error) {
    	return httprate.CanonicalizeIP(middleware.GetClientIP(r.Context())), nil
    }))
    // Directly exposed to clients (equivalent to old LimitByIP behavior):
    r.Use(middleware.ClientIPFromRemoteAddr)
    r.Use(httprate.LimitBy(requestLimit, windowLength, func(r *http.Request) (string, error) {
    	return httprate.CanonicalizeIP(middleware.GetClientIP(r.Context())), nil
    }))
  6. Migrate from deprecated Limit functions to LimitBy

    master

    The Limit, LimitAll, LimitByIP, and LimitByRealIP functions are deprecated. You should migrate to LimitBy, which requires an explicit key function as a mandatory argument. This makes the rate-limiting strategy (e.g., global, per-IP, or per-user) much more explicit.

    Replacement Pattern: LimitBy(requestLimit, windowLength, keyFn, options...)

    • For a single global bucket: Use httprate.Key("*") as the keyFn.
    • For per-IP limiting: Use a custom key function that retrieves the client IP from the request context (see the recommended pattern below).
    // Instead of Limit(10, time.Minute)
    // Use LimitBy with an explicit key function
    httprate.LimitBy(10, time.Minute, httprate.Key("*"))
  7. Rate limit by request payload

    master

    For scenarios like login endpoints, you can use httprate.NewRateLimiter to manually check limits within a handler based on a value extracted from the request body (e.g., a username).

    // Rate-limiter for login endpoint.
    loginRateLimiter := httprate.NewRateLimiter(5, time.Minute)
    
    r.Post("/login", func(w http.ResponseWriter, r *http.Request) {
    	var payload struct {
    		Username string `json:"username"`
    		Password string `json:"password"`
    	}
    	if err := json.NewDecoder(r.Body).Decode(&payload); err != nil || payload.Username == "" {
    		w.WriteHeader(400)
    		return
    	}
    
    	// Rate-limit login at 5 req/min.
    	if loginRateLimiter.RespondOnLimit(w, r, payload.Username) {
    		return
    	}
    
    	w.Write([]byte("login at 5 req/min\n"))
    })
  8. Rate limit by arbitrary keys

    master

    The KeyFunc signature func(r *http.Request) (string, error) allows you to rate limit by any value extracted from the request, such as a custom header, a user ID, or a tenant ID.

    r.Use(httprate.LimitBy(
    	100,
    	time.Minute,
    	// rate limiting by a custom header
    	func(r *http.Request) (string, error) {
    		return r.Header.Get("X-Access-Token"), nil
    	},
    ))
  9. Rate limit by IP and URL path (endpoint)

    master

    You can combine multiple keys to create more granular limits, such as limiting a specific user to a certain number of requests per specific endpoint using httprate.JoinKeys.

    // clientIPKey is the KeyFunc from the "Rate limit by client IP behind a proxy" section.
    r.Use(httprate.LimitBy(
    	10,             // requests
    	10*time.Second, // per duration
    	httprate.JoinKeys(clientIPKey, httprate.KeyByEndpoint),
    ))
  10. Configure custom error responses

    master

    If your KeyFunc or your LimitCounter (backend) returns an error, you can intercept it using httprate.WithErrorHandler to return a specific HTTP status and message.

    r.Use(httprate.LimitBy(
    	10,
    	time.Minute,
    	clientIPKey,
    	httprate.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) {
    		http.Error(w, fmt.Sprintf(`{"error": %q}`, err), http.StatusPreconditionRequired)
    	}),
    	httprate.WithLimitCounter(customBackend),
    ))
  11. Configure custom responses for rate-limited requests

    master

    By default, httprate returns HTTP 429 with a Too Many Requests body. Use httprate.WithLimitHandler to provide a custom response (e.g., JSON) when a limit is exceeded.

    r.Use(httprate.LimitBy(
    	10,
    	time.Minute,
    	clientIPKey,
    	httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
    		http.Error(w, `{"error": "Rate-limited. Please, slow down."}`, http.StatusTooManyRequests)
    	}),
    ))
  12. Pass custom increment values using WithIncrement

    master

    Use WithIncrement to attach a specific increment value to a context.Context. This allows you to control how much the rate limit counter increases for a specific request (e.g., if a request is more 'expensive' than a standard one, you can pass a value greater than 1).

    ctx := httprate.WithIncrement(r.Context(), 5)
    // Subsequent middleware using this context will increment the rate limit by 5 instead of 1