Middlewares in Go: Best practices and examples

The first code smell we encounter when writing a web application in Go is code duplication. Before processing the request, we will often need to log the request, convert app errors into HTTP 500 errors, authenticate users, etc. And we need to do most of these things for each handler.

This is the second article in the five-part series "Build Your Own Web Framework in Go":

Basics reminder

We create a very simple app from scratch with the net/http package of the standard library:

import (
  "net/http"
  "fmt"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Welcome!")
}

func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

We have a function with 2 args: a response writer and a request. In addition to having a function, we can implement the http.Handler interface to any struct.

type handler struct {}

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome!")
}

func main() {
  http.Handle("/", handler)
  http.ListenAndServe(":8080", nil)
}

Any struct with the method ServeHTTP(http.ResponseWriter, *http.Request) will be implementing http.Handler and will be usable with the Go muxer (http.Handle(pattern, handler) function).

Adding logging

We now want to add a simple log of the time spent to process each request:

func indexHandler(w http.ResponseWriter, r *http.Request) {
  t1 := time.Now()
  fmt.Fprintf(w, "Welcome!")
  t2 := time.Now()
  log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
}

func main() {
  http.HandleFunc("/", indexHandler)
  http.ListenAndServe(":8080", nil)
}

Easy enough. Here's what it will output:

[GET] / 1.43ms
[GET] /about 1.98ms

Now we add a second handler because, let's face it, there are not many apps with only one route.

func aboutHandler(w http.ResponseWriter, r *http.Request) {
  t1 := time.Now()
  fmt.Fprintf(w, "You are on the about page.")
  t2 := time.Now()
  log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
  t1 := time.Now()
  fmt.Fprintf(w, "Welcome!")
  t2 := time.Now()
  log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
}

func main() {
  http.HandleFunc("/about", aboutHandler)
  http.HandleFunc("/", indexHandler)
  http.ListenAndServe(":8080", nil)
}

Code duplication detected! We could create a function with a closure. But if we have multiple functions like that, it will become as bad as callback spaghetti in Javascript. We don't want that.

Chaining handlers

We want something like the middleware systems of Rack, Ring, Connect.js and other similar solutions. What we would like is to chain multiple handlers. We already have this kind of handlers in the standard library: http.StripPrefix(prefix, handler) and http.TimeoutHandler(handler, duration, message). They both take a handler as one of their arguments and they both return a handler. So we can write a handler and pass another handler to it.

loggingHandler(recoverHandler(indexHandler))

So a middleware would be something like func (http.Handler) http.Handler This way we pass a handler and returns a handler. At the end we have one handler and can be called with http.Handle(pattern, handler)

func main() {
  http.Handle("/", loggingHandler(recoverHandler(indexHandler)))
  http.ListenAndServe(":8080", nil)
}

But it can be cumbersome for multiple routes, repeating the stack over and over again. It would be easier to chain them in a more elegant way.

Alice

Alice is a small package to chain handlers more elegantly. Furthermore, we can create a common list of handlers and reuse them for each route like this:

func main() {
  commonHandlers := alice.New(loggingHandler, recoverHandler)
  http.Handle("/about", commonHandlers.ThenFunc(aboutHandler))
  http.Handle("/", alice.New(commonHandlers, bodyParserHandler).ThenFunc(indexHandler))
  http.ListenAndServe(":8080", nil)
}

Problem solved. We now have a middleware system that is idiomatic and use standard interfaces. Alice is 50 lines of code, so it is a very small dependency.

Chaining handlers with multiple args

But we still can't use handlers like http.StripPrefix(prefix, handler) because it is not func (http.Handler) http.Handler. Though, each time we need to use http.StripPrefix(prefix, handler) we can just make a new handler defined like func (http.Handler) http.Handler:

func myStripPrefix(h http.Handler) http.Handler {
    return http.StripPrefix("/old", h)
}

Back to our Logging handler

Now that we found a way to remove code duplication in an elegant way, we can finish to write our application. First we extract our logging code into a handler:

func loggingHandler(next http.Handler) http.Handler {
  fn := func(w http.ResponseWriter, r *http.Request) {
    t1 := time.Now()
    next.ServeHTTP(w, r)
    t2 := time.Now()
    log.Printf("[%s] %q %v\n", r.Method, r.URL.String(), t2.Sub(t1))
  }

  return http.HandlerFunc(fn)
}

We run some code before executing the next handler(s) and after we have some more code. It is very similar to Negroni but we don't need a new and redefined http.Handler interface. We stay with standard interfaces, which means less things to learn and less dependencies for an equivalent feature set.

Finally we use alice to chain loggingHandler with our other handlers:

func aboutHandler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "You are on the about page.")
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Welcome!")
}

func main() {
  commonHandlers := alice.New(loggingHandler)
  http.Handle("/about", commonHandlers.ThenFunc(aboutHandler))
  http.Handle("/", commonHandlers.ThenFunc(indexHandler))
  http.ListenAndServe(":8080", nil)
}

Duplication removed!

Let's write another middleware: Panic recovery

Another feature that is really necessary. When our code panic in production (make sure it should not but we can forget things sometimes) our application will shutdown. Even if we have a monitoring software to restart our application, we'll have an embarrassing downtime. So we must catch panics, log them and keep the application running. It's pretty easy with Go and our middleware system. We just have to make a deferred function that will recover the panic, respond with a HTTP 500 error and log the panic:

func recoverHandler(next http.Handler) http.Handler {
  fn := func(w http.ResponseWriter, r *http.Request) {
    defer func() {
      if err := recover(); err != nil {
        log.Printf("panic: %+v", err)
        http.Error(w, http.StatusText(500), 500)
      }
    }()

    next.ServeHTTP(w, r)
  }

  return http.HandlerFunc(fn)
}

Then we add it to our middleware stack:

func main() {
  commonHandlers := alice.New(loggingHandler, recoverHandler)
  http.Handle("/about", commonHandlers.ThenFunc(aboutHandler))
  http.Handle("/", commonHandlers.ThenFunc(indexHandler))
  http.ListenAndServe(":8080", nil)
}

Wrap Up

We just saw that func (http.Handler) http.Handler is a pretty simple way to define middlewares, yet it delivers everything we need. http.Handler is a standard interface and this chaining is already used in popular libraries like Gorilla and in the standard library itself. I think this is the most idiomatic way.

The two middlewares we wrote, logging and panic recovery, are rewrote in each framework I've seen despite the fact they all are pretty much the same. But most frameworks have their own version of handlers and can't really work with other middlewares that their own. We will see in the next part that there might be a reason. When we need to share values between middlewares we might need to change some things. But it is not as big of a change so there's no reason to rewrite middlewares that have already been written.