In my logging middleware (first in chain) I need to access some context that is written in some auth middleware futher down the chain and only after the handler itself is executed.
Side note: The logging middleware needs to be called first since I need to log the duration of the request including the time spend in middleware. Also the auth middleware is able to abort a request when permissions are not sufficient. in that case I need to log the failed request as well.
My problem with that is that reading the context from the http.Request
pointer does not return the auth data I would expect it to have. See the example bellow:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
const (
contextKeyUsername = "username"
)
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, contextKeyUsername, "user123")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func logMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func(start time.Time) {
ctx := r.Context()
username := ctx.Value(contextKeyUsername)
if username != nil {
fmt.Printf("user %s has accessed %s, took %d\n", username,
r.URL.Path, time.Since(start).Milliseconds())
} else {
fmt.Printf("annonyous has accessed %s, took %d\n",
r.URL.Path, time.Since(start).Milliseconds())
}
}(time.Now())
next.ServeHTTP(w, r)
})
}
func welcome(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
username := ctx.Value(contextKeyUsername)
if username != nil {
fmt.Fprintf(w, fmt.Sprintf("hello %s", username.(string)))
} else {
fmt.Fprintf(w, "hello")
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/welcome", welcome)
chain := logMiddleware(authMiddleware(mux))
http.ListenAndServe(":5050", chain)
}
While a get request to 127.0.0.1:5050/welcome
does return the expected string hello user123
, the output of the log is:
annonyous has accessed /welcome, took 0
Since the request is passed along as pointer I would have expected that at the time the defer is executed, the context would contain the expected username
value.
What am I missing here?
CodePudding user response:
WithContext
returns a shallow copy of the request, i.e. the request created by the authMiddleware
is not the same request as the one from which logMiddleware
is reading the context.
You could have the root middleware (in this case that would be the logMiddleware
) create the context-with-value and the shallow request copy, but instead of a plain string store an non-nil pointer in the context, then have the authMiddleware
use pointer indirection to assign the value to which the pointer points, then the logMiddleware
, after next
exits, can dereference that pointer to access that value.
And to avoid the unpleasant dereferencing, instead of a pointer to a string, you can use a pointer to a struct with a string field.
type ctxKey uint8
const userKey ctxKey = 0
type user struct{ name string }
func logMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u := new(user)
r = r.WithContext(context.WithValue(r.Context(), userKey, u))
defer func(start time.Time) {
if u.name != "" {
fmt.Printf("user %s has accessed %s, took %s\n", u.name, r.URL.Path, time.Since(start))
} else {
fmt.Printf("annonyous has accessed %s, took %s\n", r.URL.Path, time.Since(start))
}
}(time.Now())
next.ServeHTTP(w, r)
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if u, ok := r.Context().Value(userKey).(*user); ok {
u.name = "user123"
}
next.ServeHTTP(w, r)
})
}
func welcome(w http.ResponseWriter, r *http.Request) {
if u, ok := r.Context().Value(userKey).(*user); ok && u.name != "" {
fmt.Fprintf(w, "hello %s", u.name)
} else {
fmt.Fprintf(w, "hello")
}
}