/stack

Context-aware middleware chains for Go web applications

Primary LanguageGoMIT LicenseMIT

Stack
Build Status GoDoc

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.

Skip to the example ›

Usage

Making a chain

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.

Adding an application handler

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.

Using context

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().

Injecting context

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.

Example

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)
}

Code samples

TODO

  • 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