Skip to content

Blog


Writing Delightful HTTP Middleware in Go

July 22, 2019

|
Zohaib Sibte Hassan

Zohaib Sibte Hassan

While writing complex services in go, one typical topic that you will encounter is middleware. This topic has been discussed again, and again, and again, on internet. Essentially a middleware should allow us to:
  • Intercept a ServeHTTP call, and execute any arbitrary code.
  • Make changes to request/response flow along continuation chain.
  • Break the middleware chain, or continue onto next middleware interceptor eventually leading to real request handler.
All of this would sound very similar to what express.js middleware do. We explored  various libraries and found existing solutions that closely matched what we wanted, but they either had unnecessary extras, or were not delightful for our taste buds. It was pretty obvious that we can write express.js inspired middleware, with cleaner installation API under 20 lines of code ourselves.

The abstraction

While designing the abstraction, we first imagined how we want to write our middleware functions (referred as interceptors from this point onwards), and the answer was pretty obvious: https://gist.github.com/x0a1b/f7c64c92cfc6eae7ba1c2931bbabb475#file-middleware_example-go They look just like http.HandlerFunc, but having an extra parameter next that continues the handler chain. This would allow anyone to write interceptor as simple function similar to http.HandlerFunc that can intercept the call, do what they want, and pass on the control if they want to. Next we imagined how to hook these interceptors to our http.Handler or http.HandlerFunc. In order to do so, first thing do is define MiddlewareHandlerFunc which is simply a type of http.HandlerFunc (i.e. type MiddlewareHandlerFunc http.HandlerFunc). This will allow us to build a nicer API on top of stock http.HandlerFunc. Now given a http.HandlerFunc we want our chain-able API to look somewhat like this: https://gist.github.com/x0a1b/f7c64c92cfc6eae7ba1c2931bbabb475#file-usage_example-go Casting http.HandlerFunc to MiddlewareHandlerFunc, and then calling Intercept method to install our interceptor. The return type of Intercept is again a MiddlewareHandlerFunc, which allows us to call Intercept again. One important thing to note with Intercept scheme is the order of execution. Since calling chain(responseWriter, request) is indirectly invoking last interceptor, the execution of interceptors is reversed i.e. it goes from interceptor at tail all the way back to handler at head. Which makes perfect sense because you are intercepting the call; so you should get a chance to execute before your parent.

The simplification

While this reverse chaining system makes the abstraction more fluent, turns out most the time we have a precompiled array of interceptors that will be reused with different handlers. Also when we are defining a chain of middleware as an array, we would naturally prefer to declare them in the order of their execution (not reversed order). Let's call this array interceptors MiddlewareChain. We want our middleware chains to look somewhat like: https://gist.github.com/x0a1b/f7c64c92cfc6eae7ba1c2931bbabb475#file-array_usage_example-go Note these middleware will be invoked in same order as the appear in the chain i.e. RequestIdInterceptor and ElapsedTimeInterceptor. This adds both reusability, and readability to our code.

The implementation

Once we designed the abstraction the implementation was pretty straight forward: https://gist.github.com/x0a1b/f7c64c92cfc6eae7ba1c2931bbabb475#file-middleware-go So under 20 lines of code (excluding comments), we were able to build a nice middleware library. It's almost barebones, but the coherent abstraction of these few lines is just amazing. It enables us to write some slick middleware chains, without any fuss. Hopefully these few lines will inspire your middleware experience to be delightful as well. Like what you see? Come join us.

About the Author

Related Jobs

Location
Toronto, ON
Department
Engineering
Location
New York, NY; San Francisco, CA; Sunnyvale, CA; Los Angeles, CA; Seattle, WA
Department
Engineering
Location
San Francisco, CA; Sunnyvale, CA
Department
Engineering
Location
Seattle, WA; San Francisco, CA; Sunnyvale, CA
Department
Engineering
Location
Seattle, WA; San Francisco, CA; Sunnyvale, CA
Department
Engineering