stack

by alexedwards

alexedwards / stack

Context-aware middleware chains for Go web applications

129 Stars 13 Forks Last release: Not found MIT License 58 Commits 0 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

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

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.