From 8b0c51123f3bf63d7382ce026f65a28e6f9083ba Mon Sep 17 00:00:00 2001 From: Maxime Delporte Date: Sun, 16 Nov 2025 10:36:36 +0100 Subject: [PATCH] Enforcing a global rate limit. --- cmd/api/errors.go | 5 +++++ cmd/api/middleware.go | 17 +++++++++++++++++ cmd/api/routes.go | 3 ++- go.mod | 1 + go.sum | 2 ++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/cmd/api/errors.go b/cmd/api/errors.go index e553361..bb8339f 100644 --- a/cmd/api/errors.go +++ b/cmd/api/errors.go @@ -61,3 +61,8 @@ func (app *application) editConflictResponse(w http.ResponseWriter, r *http.Requ message := "unable to update the record due to an edit conflict, please try again" app.errorResponse(w, r, http.StatusConflict, message) } + +func (app *application) rateLimitExceededResponse(w http.ResponseWriter, r *http.Request) { + message := "rate limit exceeded" + app.errorResponse(w, r, http.StatusTooManyRequests, message) +} diff --git a/cmd/api/middleware.go b/cmd/api/middleware.go index 03fe91f..3abc444 100644 --- a/cmd/api/middleware.go +++ b/cmd/api/middleware.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "golang.org/x/time/rate" "net/http" ) @@ -22,3 +23,19 @@ func (app *application) recoverPanic(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } + +func (app *application) rateLimit(next http.Handler) http.Handler { + // Initialize a new rate limiter which allows an overage of 2 requests per second, with a maximum of 4 requests in a single `burst` + limiter := rate.NewLimiter(2, 4) + + // The function we are returning is a closure, which 'closes over' the limiter variable + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Call limiter.Allow() to see if the request is permitted, and if it's not, then we call the rateLimitExceededResponse() helper to return a 429 Too Many Requests response (we will create this helper in a minute). + if !limiter.Allow() { + app.rateLimitExceededResponse(w, r) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/cmd/api/routes.go b/cmd/api/routes.go index a4126e6..51240f8 100644 --- a/cmd/api/routes.go +++ b/cmd/api/routes.go @@ -31,5 +31,6 @@ func (app *application) routes() http.Handler { // Return the httprouter instance. // Wrap the router with the panic recovery middleware - return app.recoverPanic(router) + // Wrap the router with the rateLimit() middleware + return app.recoverPanic(app.rateLimit(router)) } diff --git a/go.mod b/go.mod index dbe9583..b0c0397 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.25.1 require ( github.com/julienschmidt/httprouter v1.3.0 // indirect github.com/lib/pq v1.10.9 // indirect + golang.org/x/time v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 0a18b5d..0ecf137 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,5 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=