avlisetan
avlisetan

Reputation: 31

Way to pass data up to parent middleware?

I've got a pretty solid grip on how to pass data from a handler to the handler it wraps, but is there a go idiomatic way to get something back from the wrapped handler? Here's a motivating example: I have an accessLogHandler and an authHandler. accessLogHandler logs every http request, with timings and other request info such as the currently logged in user's ID (if there is one). authHandler is for routes that need a logged in user, it 403's when a user isn't logged in. I want to wrap some (but perhaps not all) of my routes with the authHandler, and wrap all of my routes with the accessLogHandler. If a user is logged in, I would like my accessLogHandler to log the user info along with the access log.

Now, I have a solution I've come up with that I don't like. I'll add the code and then explain some of my issues with it.

// Log the timings of each request optionally including user data
// if there is a logged in user
func accessLogHandler(fn http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        accessLog := newAccessLog()
        ctx := context.WithValue(r.Context(), accessLogKey, accessLog)
        fn.ServeHTTP(w, r.WithContext(ctx))

        // Logs the http access, ommit user info if not set
        accessLog.Log()
    }
}

// pull some junk off the request/cookies/whatever and check if somebody is logged in
func authHandler(fn http.HandlerFunc) http.HandlerFunc {
    return func (w http.ResponseWriter, r *http.Request) {
        //Do some authorization
        user, err := auth(r)
        if err != nil{
            //No userId, don't set anything on the accesslogger
            w.WriteHeader(http.StatusForbiddend)
            return
        }
        //Success a user is logged in, let's make sure the access logger knows
        acessLog := r.Context().Value(accessLogKey).(*AccessLog)
        accessLog.Set("userID", user.ID)
        fn.ServeHTTP(w, r)
    }
}

Basically, what I'm doing here is attaching an accessLog struct to my context inside the accessLogHandler and inside the authHandler I'm reading accessLog from the context and calling accessLog.Set to inform the logger that a userID is present.

Some things I don't like about this approach:

  1. context is immutable, but I'm sticking a mutable struct on it and mutating said struct elsewhere downstream. Feels like a hack.
  2. My authHandler now has a package level dependency on the accessLog package, since I'm type asserting to *AccessLog.
  3. Ideally my authHandler would have some way of informing any part of the request stack about user data without tightly coupling itself to said parts.

Upvotes: 3

Views: 2232

Answers (1)

jmaloney
jmaloney

Reputation: 12310

Context itself is an interface, so you could create a new logger context in the logger middleware that has the methods you would need to get the behavior you are after.

Something like this:

type Logger struct{}

func (l *Logger) SetLogField(key string, value interface{}) {// set log field }
func (l *Logger) Log(){// log request}

type LoggerCtx struct {
    context.Context
    *Logger
}

func newAccessLog() *Logger {
    return &Logger{}
}

func accessLogHandler(fn http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // create new logger context
        ctx := &LoggerCtx{}
        ctx.Context = r.Context()
        ctx.Logger = newAccessLog()

        fn.ServeHTTP(w, r.WithContext(ctx))

        // Logs the http access, ommit user info if not set
        ctx.Log()
    }
}

// pull some junk off the request/cookies/whatever and check if somebody is logged in
func authHandler(fn http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        //Do some authorization
        user, err := auth(r)
        if err != nil {
            //No userId, don't set anything on the accesslogger
            w.WriteHeader(http.StatusForbiddend)
            return
        }

        //Success a user is logged in, let's make sure the access logger knows
        ctx := r.Context()

        // this could be moved - here for clarity
        type setLog interface {
            SetLogField(string, interface{})
        }

        if lctx, ok := ctx.(setLog); ok {
            lctx.SetLogField("userID", user.ID)
        }

        fn.ServeHTTP(w, r.WithContext(ctx))
    }
}

Upvotes: 1

Related Questions