From 83bb7294db49c15ad4bee3ab384075b781349dad Mon Sep 17 00:00:00 2001 From: Maxime Delporte Date: Tue, 18 Nov 2025 20:10:55 +0100 Subject: [PATCH] Configuring the Rate Limiters. --- cmd/api/main.go | 19 +++++++++++++---- cmd/api/middleware.go | 48 ++++++++++++++++++++++++------------------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 2310e1a..94974f1 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -28,17 +28,23 @@ For now, the only configuration settings will be the network port that we want t and the name of the current operating environment for the application (development, staging, production, etc.) We'll read in the configuration settings from command-line flags when the application starts. */ -// Add a db struct field to hold the configuration settings for our database connection pool. For now, this only holds the DSN, which we will read in from a command-line flag. -// Add maxOpenConns, maxIdleConns and maxIdleTime fields to hold the configuration settings for the connection pool type config struct { port int env string - db struct { - dsn string + // Add a db struct field to hold the configuration settings for our database connection pool. For now, this only holds the DSN, which we will read in from a command-line flag. + db struct { + dsn string + // Add maxOpenConns, maxIdleConns and maxIdleTime fields to hold the configuration settings for the connection pool maxOpenConns int maxIdleConns int maxIdleTime time.Duration } + // Add a new limiter struct containing fields for the requests-per-second and burst values, and a boolean field which we can use to enable/disable rate limiting altogether + limiter struct { + rps float64 + burst int + enabled bool + } } // application struct hold the dependencies for our HTTP handlers, helpers, and middleware. @@ -68,6 +74,11 @@ func main() { flag.IntVar(&cfg.db.maxIdleConns, "db-max-idle-conns", 25, "PostgreSQL max idle connections") flag.DurationVar(&cfg.db.maxIdleTime, "db-max-idle-time", 15*time.Minute, "PostgreSQL max connection idle time") + // Create command line flags to read the setting values into the config struct. Notice taht we use true as the default for the 'enabled' setting + flag.Float64Var(&cfg.limiter.rps, "limiter-rps", 2, "Rate limiter maximum requests per second") + flag.IntVar(&cfg.limiter.burst, "limiter-burst", 4, "Rate limiter maximum burst") + flag.BoolVar(&cfg.limiter.enabled, "limiter-enabled", true, "Enable rate limiter") + flag.Parse() // Initialize a new structured logger which writes log entries to standard out stream. diff --git a/cmd/api/middleware.go b/cmd/api/middleware.go index 710317c..10b8908 100644 --- a/cmd/api/middleware.go +++ b/cmd/api/middleware.go @@ -62,34 +62,40 @@ func (app *application) rateLimit(next http.Handler) http.Handler { // The function we are returning is a closure, which 'closes over' the limiter variable return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Extract the client's IP address from request. - ip, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - app.serverErrorResponse(w, r, err) - return - } + // Only carry out the check if rate limiting is enabled + if app.config.limiter.enabled { + // Extract the client's IP address from request. + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + app.serverErrorResponse(w, r, err) + return + } - // Lock the mutex to prevent this code from being executed concurrently - mu.Lock() + // Lock the mutex to prevent this code from being executed concurrently + mu.Lock() - // Check to see if the IP address already exists in the map. If it doesn't, then initialize a new rate limiter and add the IP address and limiter to he map. - if _, found := clients[ip]; !found { - clients[ip] = &client{limiter: rate.NewLimiter(2, 4)} - } + // Check to see if the IP address already exists in the map. If it doesn't, then initialize a new rate limiter and add the IP address and limiter to the map. + if _, found := clients[ip]; !found { + // Use the requests-per-second and burst values from the config struct + rps := rate.Limit(app.config.limiter.rps) + burst := app.config.limiter.burst + clients[ip] = &client{limiter: rate.NewLimiter(rps, burst)} + } - // Update the last see time for the client. - clients[ip].lastSeen = time.Now() + // Update the last see time for the client. + clients[ip].lastSeen = time.Now() - // Call the Allow() method on the rate limiter for the current IP address. If the request isn't allowed, unlock the mutex and send a 429 Too Many Requests response - if !clients[ip].limiter.Allow() { + // Call the Allow() method on the rate limiter for the current IP address. If the request isn't allowed, unlock the mutex and send a 429 Too Many Requests response + if !clients[ip].limiter.Allow() { + mu.Unlock() + app.rateLimitExceededResponse(w, r) + return + } + + // Very importantly, unlock the mutex before calling the next handler in the chain. Notice that we DON'T use defer to unlock the mutex, as that would mean that the mutex isn't unlocked until all the handlers downstream of this middleware have also returned mu.Unlock() - app.rateLimitExceededResponse(w, r) - return } - // Very importantly, unlock the mutex before calling the next handler in the chain. Notice that we DON'T use defer to unlock the mutex, as that would mean that the mutex isn't unlocked until all the handlers downstream of this middleware have also returned - mu.Unlock() - next.ServeHTTP(w, r) }) }