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.Handler by 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)For convenience ThenHandler() and ThenHandlerFunc() methods are also provided. These allow you to finish a chain with a standard http.Handler or http.HandlerFunc respectively.
For example, you could use a standard http.FileServer as 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 HandlerChain object which satisfies the http.Handler interface, and can be used with the http.DefaultServeMux and 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.Context are 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.
Data is added with 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)
})
}You retrieve data with 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 nil if a key does not exist. If you need to tell the difference between a key having a nil value 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.Context during 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.
The 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 main
import (
"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)
}- Integrating with httprouter
- More to follow
- Add more code samples (using 3rd party middleware)
- Make a
chain.Merge()method - Mirror master in v1 branch (and mention gopkg.in in README)
- Add benchmarks