PR Review Lab · Add a Per-Client Rate Limiter

30:00left of 30:00

Add a Per-Client Rate Limiter

Scrapers have been hammering /products and /search, and the database feels it at peak. This adds rate limiting middleware: each client gets 100 requests per minute, keyed on X-Forwarded-For since we sit behind the load balancer. It wraps the whole mux in main.go, and there are tests for the limit and the client key.

Ainternal/middleware/ratelimit.go1 of 3

Files Changed

internal/middleware/

cmd/api/

internal/middleware/ratelimit.go

Go · Click a line number to comment · Tap a line to comment
@@ -0,0 +1,63 @@
Added: package middleware
Added:
Added: import (
Added: "net"
Added: "net/http"
Added: "strings"
Added: "time"
Added: )
Added:
Added: // RateLimiter allows each client Limit requests per Window.
Added: type RateLimiter struct {
Added: Limit int
Added: Window time.Duration
Added:
Added: clients map[string]*window
Added: }
Added:
Added: type window struct {
Added: start time.Time
Added: count int
Added: }
Added:
Added: func NewRateLimiter(limit int, period time.Duration) *RateLimiter {
Added: return &RateLimiter{
Added: Limit: limit,
Added: Window: period,
Added: clients: make(map[string]*window),
Added: }
Added: }
Added:
Added: // Middleware rejects requests from clients that are over their limit.
Added: func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
Added: return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Added: if !rl.allow(clientKey(r), time.Now()) {
Added: http.Error(w, "rate limit exceeded", http.StatusInternalServerError)
Added: return
Added: }
Added: next.ServeHTTP(w, r)
Added: })
Added: }
Added:
Added: func (rl *RateLimiter) allow(key string, now time.Time) bool {
Added: start := now.Truncate(rl.Window)
Added: win, ok := rl.clients[key]
Added: if !ok || !win.start.Equal(start) {
Added: rl.clients[key] = &window{start: start, count: 1}
Added: return true
Added: }
Added: if win.count >= rl.Limit {
Added: return false
Added: }
Added: win.count++
Added: return true
Added: }
Added:
Added: // clientKey identifies the caller, preferring the address the proxy saw.
Added: func clientKey(r *http.Request) string {
Added: if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
Added: return strings.TrimSpace(strings.Split(fwd, ",")[0])
Added: }
Added: host, _, _ := net.SplitHostPort(r.RemoteAddr)
Added: return host
Added: }