Reputation: 31
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:
authHandler
now has a package level dependency on the accessLog
package, since I'm type asserting to *AccessLog
.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
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