From 7affd35ae4cb43f3d60a1ace67541b0425c10f19 Mon Sep 17 00:00:00 2001 From: Jonas Date: Mon, 27 Apr 2026 14:02:26 +0200 Subject: [PATCH] feat: middleware, expvar --- projects/greenlight/cmd/api/main.go | 22 ++++++++++++++- projects/greenlight/cmd/api/middleware.go | 27 ++++++++++++------- projects/greenlight/cmd/api/routes.go | 3 +++ .../cmd/examples/cors/preflight/main.go | 2 +- .../greenlight-bruno/Users/Create User.yml | 1 + 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/projects/greenlight/cmd/api/main.go b/projects/greenlight/cmd/api/main.go index 94cf216..7f91279 100644 --- a/projects/greenlight/cmd/api/main.go +++ b/projects/greenlight/cmd/api/main.go @@ -3,8 +3,10 @@ package main import ( "context" "database/sql" + "expvar" "flag" "os" + "runtime" "strings" "sync" "time" @@ -34,7 +36,8 @@ type config struct { maxOpenConns int maxIdleConns int maxIdleTime string - } // Add a new limiter struct containing fields for the requests-per-second and burst + } + // 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 { @@ -138,6 +141,23 @@ func main() { // established. logger.PrintInfo("database connection pool established", nil) + // Publish a new "version" variable in the expvar handler containing our application + // version number (currently the constant "1.0.0"). + expvar.NewString("version").Set(version) + expvar.Publish("goroutines", expvar.Func(func() any { + return runtime.NumGoroutine() + })) + + // Publish the database connection pool statistics. + expvar.Publish("database", expvar.Func(func() any { + return db.Stats() + })) + + // Publish the current Unix timestamp. + expvar.Publish("timestamp", expvar.Func(func() any { + return time.Now().Unix() + })) + app := &application{ config: cfg, logger: logger, diff --git a/projects/greenlight/cmd/api/middleware.go b/projects/greenlight/cmd/api/middleware.go index de36a43..292a52a 100644 --- a/projects/greenlight/cmd/api/middleware.go +++ b/projects/greenlight/cmd/api/middleware.go @@ -247,27 +247,36 @@ func (app *application) requirePermission(code string, next http.HandlerFunc) ht func (app *application) enableCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Add the "Vary: Origin" header. w.Header().Add("Vary", "Origin") - // Get the value of the request's Origin header. + // Add the "Vary: Access-Control-Request-Method" header. + w.Header().Add("Vary", "Access-Control-Request-Method") + origin := r.Header.Get("Origin") - // Only run this if there's an Origin request header present AND at least one - // trusted origin is configured. if origin != "" && len(app.config.cors.trustedOrigins) != 0 { - // Loop through the list of trusted origins, checking to see if the request - // origin exactly matches one of them. for i := range app.config.cors.trustedOrigins { if origin == app.config.cors.trustedOrigins[i] { - // If there is a match, then set a "Access-Control-Allow-Origin - // response header with the request origin as the value. w.Header().Set("Access-Control-Allow-Origin", origin) + + // Check if the request has the HTTP method OPTIONS and contains the + // "Access-Control-Request-Method" header. If it does, then we treat + // it as a preflight request. + if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { + // Set the necessary preflight response headers, as discussed + // previously. + w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, PUT, PATCH, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") + + // Write the headers along with a 200 OK status and return from + // the // the middleware with no further action. + w.WriteHeader(http.StatusOK) + return + } } } } - // Call the next handler in the chain. next.ServeHTTP(w, r) }) } diff --git a/projects/greenlight/cmd/api/routes.go b/projects/greenlight/cmd/api/routes.go index c259c05..00fa5b8 100644 --- a/projects/greenlight/cmd/api/routes.go +++ b/projects/greenlight/cmd/api/routes.go @@ -1,6 +1,7 @@ package main import ( + "expvar" "net/http" "github.com/julienschmidt/httprouter" @@ -27,6 +28,8 @@ func (app *application) routes() http.Handler { router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler) + // Register a new GET /debug/vars endpoint pointing to the expvar handler. + router.Handler(http.MethodGet, "/debug/vars", expvar.Handler()) return app.recoverPanic(app.enableCORS(app.rateLimit(app.authenticate(router)))) } diff --git a/projects/greenlight/cmd/examples/cors/preflight/main.go b/projects/greenlight/cmd/examples/cors/preflight/main.go index f934ea7..58d7c96 100644 --- a/projects/greenlight/cmd/examples/cors/preflight/main.go +++ b/projects/greenlight/cmd/examples/cors/preflight/main.go @@ -26,7 +26,7 @@ const html = ` 'Content-Type': 'application/json' }, body: JSON.stringify({ - email: 'alice@example.com', + email: 'grace@example.com', password: 'pa55word' }) }).then( diff --git a/projects/greenlight/docs/api/greenlight-bruno/Users/Create User.yml b/projects/greenlight/docs/api/greenlight-bruno/Users/Create User.yml index e5bdf84..0c76b9c 100644 --- a/projects/greenlight/docs/api/greenlight-bruno/Users/Create User.yml +++ b/projects/greenlight/docs/api/greenlight-bruno/Users/Create User.yml @@ -13,6 +13,7 @@ http: "name": "Grace", "email": "grace@example.com", "password": "pa55word" + } auth: inherit