Search code examples
gopointersgo-chi

Go and Pointers in http middleware


I'm trying to log some data on my web server, so I created a loggingMiddleware that serves the next request and then logs the data, I thought this way I would have all the necessary data inside the r *http.Request pointer

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // call next handler
        next.ServeHTTP(o, r)

        // get requestID
        reqID := middleware.GetReqID(r.Context())

        log.WithFields(
            log.Fields{
                "request_id":     reqID,           // drops requestID
                "method":         r.Method,        // http method
                "remote_address": r.RemoteAddr,    // remote address
                "url":            r.URL.String(),  // url used by the client to access the service
                "referer":        r.Referer(),     // Referer header if present
                "user_agent":     r.UserAgent(),   // User-Agent header
                "content_length": r.ContentLength, // Content-Length header if present"
            },
        ).Info()
    })

However for the RequestID this is true only if the RequestID middleware is mounted before the loggingMiddleware

Non-Working

...
    // get a new chi rotuter
    router = chi.NewRouter()

    // MIDDLEWARES

    // log
    // use logrus to log requests
    router.Use(LoggingMiddleware)

    // requestID
    // Generate a unique id for every request
    // ids are grouped based on client hostname
    router.Use(middleware.RequestID)
...

Working

...
    // get a new chi rotuter
    router = chi.NewRouter()

    // MIDDLEWARES

    // requestID
    // Generate a unique id for every request
    // ids are grouped based on client hostname
    router.Use(middleware.RequestID)

    // log
    // use logrus to log requests
    router.Use(LoggingMiddleware)
...

Is this the expected behavior? Should the r *http.Request pointer point to the "updated" version of the request? Is there a way to get around this? Because if I want, for example, extract a username from a JWT token and put it in the r.Context() so I can log it later, this would require a separate middleware to be mounted before the loggingMiddleware.

Sorry for my english, please ask if there's something not clear.

Thanks


Solution

  • middleware.RequestID adds the request ID to the request context by using http.Request.WithContext:

    ctx = context.WithValue(ctx, RequestIDKey, requestID)
    next.ServeHTTP(w, r.WithContext(ctx))
    

    Per the documentation:

    WithContext returns a shallow copy of r with its context changed to ctx. The provided ctx must be non-nil.

    Therefore, because it "returns a shallow copy of r", and it is (a pointer to) this shallow copy which it passes to next.ServeHTTP, if middleware.RequestID is mounted second, then the r in LoggingMiddleware is pointing to a different *http.Request than the one which contains the modified context, and so its context will not contain the request ID. If, on the other hand, middleware.RequestID is mounted first, then LoggingMiddleware will receive a pointer to the shallow copy that r.WithContext returned, and everything will work as expected.