Alice – Painless Middleware Chaining for Go
May 25, 2014
According to a recent
thread
on Reddit,
many people like it simple
when doing web development in Go
and use net/http
with useful addons
(like the Gorilla toolkit)
instead of a full-fledged framework.
Such applications often make use of middleware, but wrapping lots of layers of handlers can become messy in the long run:
final := gzipHandler(rateLimitHandler(securityHandler(authHandler(myApp))))
Sure, that works. But then you want to remove a handler, add one or reorder them. Suddenly, you're drowning in parentheses.
Alice (available on GitHub)
was created as a solution to simplify chaining
while remaining flexible
and playing nice with the existing net/http
middleware.
It's not a framework, a mux or a toolkit. Its sole functionality is to let you create the same middleware chain like this:
final := alice.New(gzipHandler, ratelimitHandler,
securityHandler, authHandler).Then(myApp)
Flaws of other approaches
Many might point out that there are existing solutions for chaining middleware. That's true, but any of the solutions I had looked into had at least one thing that I thought should be done in a differently.
The recent Negroni package allows handlers to be added as middleware like this:
n := negroni.Classic()
// func (n *Negroni) UseHandler(handler http.Handler)
n.UseHandler(myMiddleware)
See the fault here? Traditional net/http
middleware
wrap the next handler so the wrapper can have a total control.
Here, there's simply no handler to pass in.
You can't pass in the Negroni instance as it would result in
infinite recursion (Negroni calls middleware calls Negroni).
Negroni has its own mechanism for control flow
(next()
to call the following handlers),
but you have to modify your middleware to fully utilize it,
which is not ideal.
go-httppipe has the same problem: it's suited for successive handlers, but not wrapper-type middleware.
go-stackbuilder
forcibly uses http.ServeMux
as the main handler,
instead of http.Handler
.
func New(mux *http.ServeMux) Builder {
return Builder{mux}
}
func Build(hs ...interface{}) http.Handler {
return New(http.DefaultServeMux).Build(hs...)
}
That is not ideal as one might like to bypass the default mux completely, especially when using an alternative one.
Muxchain, like Negroni, doesn't provide a reference to the next handler.
Matt Silverlock's use.go snippet came closest to what I wanted. My only complaint is that the ordering of handlers here is counter-intuitive. Reading the chaining code makes it obvious that
use(myApp, csrf, logging, recovery)
is equivalent to this code:
recovery(logging(csrf(myApp)))
and this request cycle:
recovery -> logging -> csrf -> myApp
So, a reversed order from what you've written in your code.
How Alice is better
Alice adopts Matt's model and fixes the small imperfections.
It still requires middleware constructors of form func (http.Handler) http.Handler
,
but requests now flow exactly the way you order your handlers.
This code:
alice.Chain(recovery, logging, csrf).Then(myApp)
will result in this request cycle:
recovery -> logging -> csrf -> myApp
Here, recovery
receives the request and has a reference to logging
.
It may or may not call logging
:
it's completely up to recovery
,
just as without Alice.
One more thing
Deciding on a unified constructor for middleware is the main reason why creating such a convenient API for chaining is even possible.
However, limiting ourselves to one function signature has a drawback. Many middleware have settings one might want (or have) to set. Not only does that not fit into our chosen signature, there is no standard way adopted by developers.
Middleware found in the standard library take the options as additional arguments to the same function:
handler = http.StripPrefix("/old/", handler)
Throttled constructs a rate limiting strategy which has a method to wrap our handler:
th := throttled.RateLimit(throttled.PerMin(30),
&throttled.VaryBy{RemoteAddr: true},
store.NewMemStore(1000))
handler := th.Throttle(handler)
And nosurf has additional methods on the handler:
handler := nosurf.New(handler)
handler.ExemptPath("/")
handler.SetFailureHandler(http.NotFoundHandler())
It's nearly impossible for Alice to solve this without resorting to ugly reflection tricks, so it doesn't try to. Instead, when in need of customization, one should create their own constructor.
func myStripPrefix(h http.Handler) http.Handler {
return http.StripPrefix("/old/", h)
}
This now complies to the constructor interface, so we can plug it into Alice.
alice.New(myStripPrefix).Then(myApp)
Prove me wrong, though
Alice was born in a matter of minutes and based on my own perception of what's right and what's not. Just like I've found other solutions non-ideal, some might find inherent flaws in how Alice works. Although it's unlikely that the very essence of how Alice functions will change, any feedback is welcome.