Home > Software design >  Accessing HTTP Request context after handler
Accessing HTTP Request context after handler

Time:10-06

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")
    }
}

https://go.dev/play/p/N7vmjQ7iLM1

  • Related