Need help with stack?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

alexedwards
133 Stars 14 Forks MIT License 58 Commits 1 Opened issues

Description

Context-aware middleware chains for Go web applications

Services available

!
?

Need anything else?

Contributors list

# 71,284
Go
Shell
golang
csrf
56 commits
# 139,298
Go
SQL
plantum...
Amazon ...
1 commit

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.