The http.Handler wrapper technique in #golang UPDATED

tl;dr: functions that take an http.Handler and return a new one can do things before and/or after the handler is called, and even decide whether to call the original handler at all.

If you’re building web services using Go (if you’re not, why not?) and you’re not using any middleware packages (and even if you are), then you need to understand the power of wrapping http.Handler types.

An http.Handler wrapper is a function that has one input argument and one output argument, both of type http.Handler.

Wrappers have the following signature:

func(http.Handler) http.Handler

The idea is that you take in an http.Handler and return a new one that does something else before and/or after calling the ServeHTTP method on the original. For example, a simple logging wrapper might look like this:

func log(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r
*http.Request) {
log.Println("Before")
h.ServeHTTP(w, r) // call original
log.Println("After")
})
}

Here, our log function returns a new handler (remember that http.HandlerFunc is a valid http.Handler too) that will print the “Before” string, and call the original handler before printing out the “After” string.

Now, wherever I pass my original http.Handler I can wrap it such that:

http.HandleFunc("/path", handleThing)

becomes

http.HandleFunc("/path", log(handleThing))

When would you use wrappers?

This approach can be used to address lots of different situations, including but not limited to:

  • Logging and tracing
  • Validating the request; such as checking authentication credentials
  • Writing common response headers

To call or not to call

Wrappers get to decide whether to call the original handler or not. If they want to, they can even intercept the request and response on their own. Say a key URL parameter is mandatory in our API:

func checkAPIKey(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if len(r.URL.Query().Get("key") == 0) {
http.Error(w, "missing key", http.StatusUnauthorized)
return // don't call original handler
}
h.ServeHTTP(w, r)
})
}

The checkAPIKey wrapper will make sure there is a key, and if there isn’t, it will return with an Unauthorized error. It could be extended to validate the key in a datastore, or ensure the caller is within acceptable rate limits etc.

Deferring

Using Go’s defer statement we can add code that we can be sure will run whatever happens inside our original handler:

func log(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Before")
defer log.Println("After")
h.ServeHTTP(w, r)
})
}

Now, even if the code inside our handler panics, we’ll still see the “After” line printed.

Passing arguments to the wrappers

We can pass additional arguments into our wrapper functions if we want our wrappers to be reused with slight variants.

Our checkAPIKey wrapper could be changed to support checking for the presence of any URL parameters:

func MustParams(h http.Handler, params ...string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){

q := r.URL.Query()
for _, param := range params {
if len(q.Get(param)) == 0 {
http.Error(w, "missing "+param, http.StatusBadRequest)
return // exit early
}
}
h.ServeHTTP(w, r) // all params present, proceed
})
}

We can use MustParams to insist on different parameters by calling it with different arguments. The params argument inside MustParams will be captured in the closure of the function we return, so there’s no need to add a struct to store state here:

http.Handler("/user", MustParams(handleUser, "key", "auth"))
http.Handler("/group", MustParams(handleGroup, "key"))
http.Handler("/items", MustParams(handleSearch, "key", "q"))

Wrappers within wrappers

Given the self-similar nature of this pattern, and the fact that we haven’t changed the http.Handler signature at all, we are able to easily chain wrappers as well as nest them in interesting ways.

If we have a MustAuth wrapper that will validate an auth token for a request, it might well insist on the auth parameter being present. So we can use the MustParams wrapper inside our MustAuth one:

func MustAuth(h http.Handler) http.Handler {
checkauth := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
    err := validateAuth(r.URL.Query().Get("auth"))
if err != nil {
http.Error(w, "bad auth param", http.StatusUnauthorized)
}
    h.ServeHTTP(w, r)
  })
return MustParams(checkauth, "auth")
}

Intercepting the ResponseWriter

The http.ResponseWriter type is an interface, which means we can intercept a request and swap it for a different object — provided our object satisfies the same interface.

You might decide to do this if you want to capture the response body, perhaps to log it:

func log(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w := NewResponseLogger(w)
h.ServeHTTP(w, r)
})
}

The NewResponseLogger type would provide its own Write function and log the data:

func (r *ResponseLogger) Write(b []byte) (int, error) {
log.Print(string(b)) // log it out
return r.w.Write(b) // pass it to the original ResponseWriter
}

It logs the string out, and also writes the bytes to the original http.ResponseWriter, so that the caller of the API still gets the response.

Handlers, all the way down

Aside from just wrapping individual handlers as we have done so far, since everything is an http.Handler, we can wrap entire servers in one go:

http.ListenAndServe(addr, log(server))

More on writing middleware

If you’re interested in learning more about writing middleware in Go, check out Writing middleware in #golang and how Go makes it so much fun.

Questions?

If you have any questions about specific use cases, tweet me @matryer and I’ll be happy to help.

Buy my book, obviously

Learn more about the practicalities of Go with Go Programming Blueprints: Second Edition.

If you liked the first edition — you’ll love this one.

If you didn’t love the first edition — then you might love this one.

If you hated the first edition — buy the Second Edition for your enemies.