Context-aware middleware chains for Go web applications
Stack provides an easy way to chain your HTTP middleware and handlers together and to pass request-scoped context between them. It's essentially a context-aware version of Alice.
Middleware chains are constructed with
stack.New():
stack.New(middlewareOne, middlewareTwo, middlewareThree)
You can also store middleware chains as variables, and then
Append()to them:
stdStack := stack.New(middlewareOne, middlewareTwo) extStack := stdStack.Append(middlewareThree, middlewareFour)
Your middleware should have the signature
func(*stack.Context, http.Handler) http.Handler. For example:
func middlewareOne(ctx *stack.Context, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // do something middleware-ish, accessing ctx next.ServeHTTP(w, r) }) }
You can also use middleware with the signature
func(http.Handler) http.Handlerby adapting it with
stack.Adapt(). For example, if you had the middleware:
func middlewareTwo(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // do something else middleware-ish next.ServeHTTP(w, r) }) }
You can add it to a chain like this:
stack.New(middlewareOne, stack.Adapt(middlewareTwo), middlewareThree)
See the codes samples for real-life use of third-party middleware with Stack.
Application handlers should have the signature
func(*stack.Context, http.ResponseWriter, *http.Request). You add them to the end of a middleware chain with the
Then()method.
So an application handler like this:
func appHandler(ctx *stack.Context, w http.ResponseWriter, r *http.Request) { // do something handler-ish, accessing ctx }
Is added to the end of a middleware chain like this:
stack.New(middlewareOne, middlewareTwo).Then(appHandler)
ThenHandler()and
ThenHandlerFunc()methods are also provided. These allow you to finish a chain with a standard
http.Handleror
http.HandlerFuncrespectively.
For example, you could use a standard
http.FileServeras the application handler:
fs := http.FileServer(http.Dir("./static/")) http.Handle("/", stack.New(middlewareOne, middlewareTwo).ThenHandler(fs))
Once a chain is 'closed' with any of these methods it is converted into a
HandlerChainobject which satisfies the
http.Handlerinterface, and can be used with the
http.DefaultServeMuxand many other routers.
Request-scoped data (or context) can be passed through the chain by storing it in
stack.Context. This is implemented as a pointer to a
map[string]interface{}and scoped to the goroutine executing the current HTTP request. Operations on
stack.Contextare protected by a mutex, so if you need to pass the context pointer to another goroutine (say for logging or completing a background process) it is safe for concurrent use.
Context.Put(). The first parameter is a string (which acts as a key) and the second is the value you need to store. For example:
func middlewareOne(ctx *stack.Context, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx.Put("token", "c9e452805dee5044ba520198628abcaa") next.ServeHTTP(w, r) }) }
Context.Get(). Remember to type assert the returned value into the type you're expecting.
func appHandler(ctx *stack.Context, w http.ResponseWriter, r *http.Request) { token, ok := ctx.Get("token").(string) if !ok { http.Error(w, http.StatusText(500), 500) return } fmt.Fprintf(w, "Token is: %s", token) }
Note that
Context.Get()will return
nilif a key does not exist. If you need to tell the difference between a key having a
nilvalue and it explicitly not existing, please check with
Context.Exists().
Keys (and their values) can be deleted with
Context.Delete().
It's possible to inject values into
stack.Contextduring a request cycle but before the chain starts to be executed. This is useful if you need to inject parameters from a router into the context.
Inject()function returns a new copy of the chain containing the injected context. You should make sure that you use this new copy – not the original – for subsequent processing.
Here's an example of a wrapper for injecting httprouter params into the context:
func InjectParams(hc stack.HandlerChain) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { newHandlerChain := stack.Inject(hc, "params", ps) newHandlerChain.ServeHTTP(w, r) } }
A full example is available in the code samples.
package mainimport ( "net/http" "github.com/alexedwards/stack" "fmt" )
func main() { stk := stack.New(token, stack.Adapt(language))
http.Handle("/", stk.Then(final))
http.ListenAndServe(":3000", nil) }
func token(ctx *stack.Context, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx.Put("token", "c9e452805dee5044ba520198628abcaa") next.ServeHTTP(w, r) }) }
func language(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Language", "en-gb") next.ServeHTTP(w, r) }) }
func final(ctx *stack.Context, w http.ResponseWriter, r *http.Request) { token, ok := ctx.Get("token").(string) if !ok { http.Error(w, http.StatusText(500), 500) return } fmt.Fprintf(w, "Token is: %s", token) }
chain.Merge()method