This is the fifth post in a series about writing REST servers in Go. Here is a list of posts in the series:

In this part we're going to talk about middleware. In an earlier post on the Life of an HTTP request in a Go server, I've described the basic mechanics of how middleware works in Go. It's an important pre-requisite; please read it, if you haven't yet.

Basic middleware for our task server

It's time to revisit our task server once again! The following example is based on the basic stdlib-only task server developed in part 1. We'll talk about adding middleware to the server and the different options we have for integrating it with the rest of the code. The complete code for the task server discussed below is available here.

Our original task server had a log.Printf call at the beginning of every handler to log the request being handled. This is something middleware can do with less code duplication. Here's a simple logging middleware:

func Logging(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    start := time.Now()
    next.ServeHTTP(w, req)
    log.Printf("%s %s %s", req.Method, req.RequestURI, time.Since(start))
  })
}

In addition to logging the request method and URI, this middleware calculates how long the handler took to complete its work and logs that as well.

To connect this middleware to our handlers, here's how main would look:

func main() {
  mux := http.NewServeMux()
  server := NewTaskServer()

  mux.HandleFunc("POST /task/", server.createTaskHandler)
  mux.HandleFunc("GET /task/", server.getAllTasksHandler)
  mux.HandleFunc("DELETE /task/", server.deleteAllTasksHandler)
  mux.HandleFunc("GET /task/{id}/", server.getTaskHandler)
  mux.HandleFunc("DELETE /task/{id}/", server.deleteTaskHandler)
  mux.HandleFunc("GET /tag/{tag}/", server.tagHandler)
  mux.HandleFunc("GET /due/{year}/{month}/{day}/", server.dueHandler)

  handler := middleware.Logging(mux)
  handler = middleware.PanicRecovery(handler)

  log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), handler))
}

Here the middleware is installed globally, affecting all handlers. Middleware could also be easily installed on a per-route basis; for example, if we only wanted logging to happen on server.tagHandler, we could do [1]:

func main() {
  mux := http.NewServeMux()
  server := NewTaskServer()
  // ... other handlers as before
  mux.Handle("/tag/", middleware.Logging(http.HandlerFunc(server.tagHandler)))
  // ... other handlers as before

  log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), mux))
}

It's also possible to mix and match: some middleware could be per-route, while other middleware could be global. Note that there's a difference in the order the middleware is executed relative to the mux in the two examples above; can you spot it?

In the first example, the order is:

request --> [Logging] --> [Mux] --> [Handler]

While in the second example, for /tag/ it's:

request --> [Mux] --> [Logging] --> [tagHandler]

Generally, it's a good idea to keep track of the order our middleware is executed in. In this case the order between the logging middleware and the mux doesn't matter too much, but in some cases order could be important.

Adding more middleware

Let's add some more middleware to our server. In Life of an HTTP request in a Go server, I mentioned how net/http recovers from panics in handlers by closing the client's connection and logging the error. If we want to do something different, we have to write our own middleware; let's give it a try:

func PanicRecovery(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
    defer func() {
      if err := recover(); err != nil {
        http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
        log.Println(string(debug.Stack()))
      }
    }()
    next.ServeHTTP(w, req)
  })
}

This middleware attaches a defer to a handler; the deferred code recovers from a panic and writes an internal error (HTTP status 500) response back to the client, while logging the panic's stack trace.

And here's main again, with the middleware chain set up:

func main() {
  mux := http.NewServeMux()
  server := NewTaskServer()
  // ... registered handlers, as before

  handler := middleware.Logging(mux)
  handler = middleware.PanicRecovery(handler)

  log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), handler))
}

The middleware execution order is now:

request --> [Panic Recovery] --> [Logging] --> [Mux] --> [tagHandler]

As before, it's easy to mix and match; we could set PanicRecovery only on some of routes, for example, while setting Logging on all the routes.

Creating middleware chains

As we've just seen, when adding middleware to a server we have to be aware of the order of execution; this is true for both global and per-route middleware. It's not surprising, then, that several packages popped up to help us define middleware "chains" in a slightly more ergonomic manner. Such packages also typically let us reuse chains between different routes. An example of such a package is alice.

As usual, a word of caution about dependencies: unless you're in a real hurry and don't care much about long-term readability and maintenance of the code, be very careful in heaping additional dependencies on your project. Especially if the benefits they bring are small. If something like alice feels much more natural to you - go for it. Otherwise, start with writing your custom code (just like in our example) and consider switching later if the need arises.

In any case, if you're using a router package like gorilla/mux or a full-fledged framework like Gin, these have their own tools for setting up middleware.

Middleware with gorilla/mux

When using the gorilla/mux router package, we get some support for middleware included. The mux.Router type has a Use(...) method which can be used to easily set up global middleware chains. Moreover, the gorilla/handlers package includes some ready-made middleware handlers [2]. For example, a panic-recovery and a logging middleware are already included, along with a few others.

Here's a concrete code sample (the full server is available here):

func main() {
  router := mux.NewRouter()
  router.StrictSlash(true)
  server := NewTaskServer()

  router.HandleFunc("/task/", server.createTaskHandler).Methods("POST")
  router.HandleFunc("/task/", server.getAllTasksHandler).Methods("GET")
  router.HandleFunc("/task/", server.deleteAllTasksHandler).Methods("DELETE")
  router.HandleFunc("/task/{id:[0-9]+}/", server.getTaskHandler).Methods("GET")
  router.HandleFunc("/task/{id:[0-9]+}/", server.deleteTaskHandler).Methods("DELETE")
  router.HandleFunc("/tag/{tag}/", server.tagHandler).Methods("GET")
  router.HandleFunc("/due/{year:[0-9]+}/{month:[0-9]+}/{day:[0-9]+}/", server.dueHandler).Methods("GET")

  // Set up logging and panic recovery middleware.
  router.Use(func(h http.Handler) http.Handler {
    return handlers.LoggingHandler(os.Stdout, h)
  })
  router.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true)))

  log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), router))
}

The main function is very similar to the original server using gorilla/mux in part 2, with the addition of two router.Use calls where we set up the middleware. I made separate Use calls for clarity, though Use can accept an arbitrary number of handlers to chain one after another.

The panic recovery middleware is straightforward to use, and demonstrates an interesting technique for configuring middleware using functional options. In this case we're configuring it to log the stack when a panic is recovered (the default is false).

The handlers.LoggingHandler middleware's API is a bit funky and we need a small adapter function to hook it into router.Use. It's not clear why it was designed this way; IMHO passing an io.Writer could have been accomplished using a functional option similarly to RecoveryHandler.

This example demonstrates how to set up global middleware (affecting the whole router); how can we set up per-route middleware with gorilla/mux?

One way would be exactly similar to what we did with the standard-library option in the previous example. An alternative is to use gorilla/mux subrouters with Use. I found the second method slightly convoluted if all you need is to add some middleware to a single path, but if your routing is already factored into several subrouters, the incremental addition may be trivial.

Middleware with gin

Let's now revisit our Gin-based task server from part 3. As specified in that post, when we create a new Gin instance with gin.Default(), some default middleware is already registered - specifically logging and panic recovery.

We can also achieve the same effect less automatically by instantiating gin.New (which adds no middleware) and then adding middleware manually:

func main() {
  // Set up middleware for logging and panic recovery explicitly.
  router := gin.New()
  router.Use(gin.Logger())
  router.Use(gin.Recovery())

  server := NewTaskServer()

  router.POST("/task/", server.createTaskHandler)
  router.GET("/task/", server.getAllTasksHandler)
  router.DELETE("/task/", server.deleteAllTasksHandler)
  router.GET("/task/:id", server.getTaskHandler)
  router.DELETE("/task/:id", server.deleteTaskHandler)
  router.GET("/tag/:tag", server.tagHandler)
  router.GET("/due/:year/:month/:day", server.dueHandler)

  router.Run("localhost:" + os.Getenv("SERVERPORT"))
}

Gin's Use method lets us attach a middleware chain to the router [3]. Just like Gin's handlers, Gin middleware does not have the standard middleware signature; instead, it's defined in package gin as:

type HandlerFunc func(*Context)

So we'd need an adapter to attach a standard-signature middleware to Gin.

If you're using gin, the gin-contrib GitHub organization has a large collection of middleware modules you could reuse for your application.

Other uses of middleware

The middleware pattern is versatile and is being widely used in REST servers for a variety of tasks. In this post's examples, I've only shown some basic logging and panic recovery middleware since I wanted to focus on the mechanism rather than on a wide survey of the use cases.

In the wild, you'll find middleware for standardized checking of requests, CORS, many variants of logging, compression, sessions, tracing, caching, encryption and authentication. I'll be covering authentication in much more detail in a future post in this series.

Closing words

This post covered the middleware pattern in detail, focusing on how to integrate it into REST servers with custom stdlib-only code, gorilla/mux routing and a full-fledged framework like Gin. My hope is that after reading it, you'll be able to understand how middleware works and how to use it in your own projects.

A word of caution: middleware is not all sparkles and rainbows. As with any pattern, it should not be overused. Middleware complicates the flow of a request through the server, making code reading and debugging more challenging. I'd strongly recommend defining all your middleware in a single place and avoid layers of abstraction where middleware gets tacked onto routes dynamically, conditionally, or within other middleware. Your future debugging self will be thankful.


[1]Note that we have to call mux.Handle instead of mux.HandleFunc in this case, because our middleware functions return an http.Handler, not an http.HandlerFunc. For a similar (but inverse) reason, when passing our handler into the middleware we have to adapt it with http.HandlerFunc.
[2]Custom-written middleware like our code earlier in this post is also easy to use with gorilla/mux, due to the standard interfaces net/http uses for handlers.
[3]Per-path middleware in Gin is easily accomplished by using router groups; each group can have its own middleware registered.