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.Handle("/path", handleThing)
becomes
http.Handle("/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.