├── .travis.yml ├── LICENSE ├── README.md ├── context.go ├── context_test.go ├── stack.go └── stack_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.1 5 | - 1.2 6 | - 1.3 7 | - 1.4 8 | - tip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alex Edwards 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stack
[![Build Status](https://travis-ci.org/alexedwards/stack.svg?branch=master)](https://travis-ci.org/alexedwards/stack) [![GoDoc](http://godoc.org/github.com/alexedwards/stack?status.png)](http://godoc.org/github.com/alexedwards/stack) 2 | 3 | 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](https://github.com/justinas/alice). 4 | 5 | [Skip to the example ›](#example) 6 | 7 | ### Usage 8 | 9 | #### Making a chain 10 | 11 | Middleware chains are constructed with [`stack.New()`](http://godoc.org/github.com/alexedwards/stack#New): 12 | 13 | ```go 14 | stack.New(middlewareOne, middlewareTwo, middlewareThree) 15 | ``` 16 | 17 | You can also store middleware chains as variables, and then [`Append()`](http://godoc.org/github.com/alexedwards/stack#Chain.Append) to them: 18 | 19 | ```go 20 | stdStack := stack.New(middlewareOne, middlewareTwo) 21 | extStack := stdStack.Append(middlewareThree, middlewareFour) 22 | ``` 23 | 24 | Your middleware should have the signature `func(*stack.Context, http.Handler) http.Handler`. For example: 25 | 26 | ```go 27 | func middlewareOne(ctx *stack.Context, next http.Handler) http.Handler { 28 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 29 | // do something middleware-ish, accessing ctx 30 | next.ServeHTTP(w, r) 31 | }) 32 | } 33 | ``` 34 | 35 | You can also use middleware with the signature `func(http.Handler) http.Handler` by adapting it with [`stack.Adapt()`](http://godoc.org/github.com/alexedwards/stack#Adapt). For example, if you had the middleware: 36 | 37 | ```go 38 | func middlewareTwo(next http.Handler) http.Handler { 39 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 | // do something else middleware-ish 41 | next.ServeHTTP(w, r) 42 | }) 43 | } 44 | ``` 45 | 46 | You can add it to a chain like this: 47 | 48 | ```go 49 | stack.New(middlewareOne, stack.Adapt(middlewareTwo), middlewareThree) 50 | ``` 51 | 52 | See the [codes samples](#code-samples) for real-life use of third-party middleware with Stack. 53 | 54 | #### Adding an application handler 55 | 56 | 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()`](http://godoc.org/github.com/alexedwards/stack#Chain.Then) method. 57 | 58 | So an application handler like this: 59 | 60 | ```go 61 | func appHandler(ctx *stack.Context, w http.ResponseWriter, r *http.Request) { 62 | // do something handler-ish, accessing ctx 63 | } 64 | ``` 65 | 66 | Is added to the end of a middleware chain like this: 67 | 68 | ```go 69 | stack.New(middlewareOne, middlewareTwo).Then(appHandler) 70 | ``` 71 | 72 | For convenience [`ThenHandler()`](http://godoc.org/github.com/alexedwards/stack#Chain.ThenHandler) and [`ThenHandlerFunc()`](http://godoc.org/github.com/alexedwards/stack#Chain.ThenHandlerFunc) methods are also provided. These allow you to finish a chain with a standard `http.Handler` or `http.HandlerFunc` respectively. 73 | 74 | For example, you could use a standard `http.FileServer` as the application handler: 75 | 76 | ```go 77 | fs := http.FileServer(http.Dir("./static/")) 78 | http.Handle("/", stack.New(middlewareOne, middlewareTwo).ThenHandler(fs)) 79 | ``` 80 | 81 | Once a chain is 'closed' with any of these methods it is converted into a [`HandlerChain`](http://godoc.org/github.com/alexedwards/stack#HandlerChain) object which satisfies the `http.Handler` interface, and can be used with the `http.DefaultServeMux` and many other routers. 82 | 83 | #### Using context 84 | 85 | 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. 86 | 87 | Data is added with [`Context.Put()`](http://godoc.org/github.com/alexedwards/stack#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: 88 | 89 | ```go 90 | func middlewareOne(ctx *stack.Context, next http.Handler) http.Handler { 91 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 92 | ctx.Put("token", "c9e452805dee5044ba520198628abcaa") 93 | next.ServeHTTP(w, r) 94 | }) 95 | } 96 | ``` 97 | 98 | You retrieve data with [`Context.Get()`](http://godoc.org/github.com/alexedwards/stack#Context.Get). Remember to type assert the returned value into the type you're expecting. 99 | 100 | ```go 101 | func appHandler(ctx *stack.Context, w http.ResponseWriter, r *http.Request) { 102 | token, ok := ctx.Get("token").(string) 103 | if !ok { 104 | http.Error(w, http.StatusText(500), 500) 105 | return 106 | } 107 | fmt.Fprintf(w, "Token is: %s", token) 108 | } 109 | ``` 110 | 111 | 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()`](http://godoc.org/github.com/alexedwards/stack#Context.Exists). 112 | 113 | Keys (and their values) can be deleted with [`Context.Delete()`](http://godoc.org/github.com/alexedwards/stack#Context.Delete). 114 | 115 | #### Injecting context 116 | 117 | 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. 118 | 119 | The [`Inject()`](http://godoc.org/github.com/alexedwards/stack#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. 120 | 121 | Here's an example of a wrapper for injecting [httprouter](https://github.com/julienschmidt/httprouter) params into the context: 122 | 123 | ```go 124 | func InjectParams(hc stack.HandlerChain) httprouter.Handle { 125 | return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 126 | newHandlerChain := stack.Inject(hc, "params", ps) 127 | newHandlerChain.ServeHTTP(w, r) 128 | } 129 | } 130 | ``` 131 | 132 | A full example is available in the [code samples](#code-samples). 133 | 134 | ### Example 135 | 136 | ```go 137 | package main 138 | 139 | import ( 140 | "net/http" 141 | "github.com/alexedwards/stack" 142 | "fmt" 143 | ) 144 | 145 | func main() { 146 | stk := stack.New(token, stack.Adapt(language)) 147 | 148 | http.Handle("/", stk.Then(final)) 149 | 150 | http.ListenAndServe(":3000", nil) 151 | } 152 | 153 | func token(ctx *stack.Context, next http.Handler) http.Handler { 154 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 155 | ctx.Put("token", "c9e452805dee5044ba520198628abcaa") 156 | next.ServeHTTP(w, r) 157 | }) 158 | } 159 | 160 | func language(next http.Handler) http.Handler { 161 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 162 | w.Header().Set("Content-Language", "en-gb") 163 | next.ServeHTTP(w, r) 164 | }) 165 | } 166 | 167 | func final(ctx *stack.Context, w http.ResponseWriter, r *http.Request) { 168 | token, ok := ctx.Get("token").(string) 169 | if !ok { 170 | http.Error(w, http.StatusText(500), 500) 171 | return 172 | } 173 | fmt.Fprintf(w, "Token is: %s", token) 174 | } 175 | ``` 176 | 177 | ### Code samples 178 | 179 | * [Integrating with httprouter](https://gist.github.com/alexedwards/4d20c505f389597c3360) 180 | * *More to follow* 181 | 182 | ### TODO 183 | 184 | - Add more code samples (using 3rd party middleware) 185 | - Make a `chain.Merge()` method 186 | - Mirror master in v1 branch (and mention gopkg.in in README) 187 | - Add benchmarks 188 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Context struct { 8 | mu sync.RWMutex 9 | m map[string]interface{} 10 | } 11 | 12 | func NewContext() *Context { 13 | m := make(map[string]interface{}) 14 | return &Context{m: m} 15 | } 16 | 17 | func (c *Context) Get(key string) interface{} { 18 | if !c.Exists(key) { 19 | return nil 20 | } 21 | c.mu.RLock() 22 | defer c.mu.RUnlock() 23 | return c.m[key] 24 | } 25 | 26 | func (c *Context) Put(key string, val interface{}) *Context { 27 | c.mu.Lock() 28 | defer c.mu.Unlock() 29 | c.m[key] = val 30 | return c 31 | } 32 | 33 | func (c *Context) Delete(key string) *Context { 34 | c.mu.Lock() 35 | defer c.mu.Unlock() 36 | delete(c.m, key) 37 | return c 38 | } 39 | 40 | func (c *Context) Exists(key string) bool { 41 | c.mu.RLock() 42 | defer c.mu.RUnlock() 43 | _, ok := c.m[key] 44 | return ok 45 | } 46 | 47 | func (c *Context) copy() *Context { 48 | nc := NewContext() 49 | c.mu.RLock() 50 | defer c.mu.RUnlock() 51 | for k, v := range c.m { 52 | nc.m[k] = v 53 | } 54 | return nc 55 | } 56 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import "testing" 4 | 5 | func TestGet(t *testing.T) { 6 | ctx := NewContext() 7 | ctx.m["flip"] = "flop" 8 | ctx.m["bish"] = nil 9 | 10 | val := ctx.Get("flip") 11 | assertEquals(t, "flop", val) 12 | 13 | val = ctx.Get("bish") 14 | assertEquals(t, nil, val) 15 | } 16 | 17 | func TestPut(t *testing.T) { 18 | ctx := NewContext() 19 | 20 | ctx.Put("bish", "bash") 21 | assertEquals(t, "bash", ctx.m["bish"]) 22 | } 23 | 24 | func TestDelete(t *testing.T) { 25 | ctx := NewContext() 26 | ctx.m["flip"] = "flop" 27 | 28 | ctx.Delete("flip") 29 | assertEquals(t, nil, ctx.m["flip"]) 30 | } 31 | 32 | func TestCopy(t *testing.T) { 33 | ctx := NewContext() 34 | ctx.m["flip"] = "flop" 35 | 36 | ctx2 := ctx.copy() 37 | ctx2.m["bish"] = "bash" 38 | assertEquals(t, nil, ctx.m["bish"]) 39 | assertEquals(t, "bash", ctx2.m["bish"]) 40 | } 41 | 42 | func TestExists(t *testing.T) { 43 | ctx := NewContext() 44 | ctx.m["flip"] = "flop" 45 | 46 | assertEquals(t, true, ctx.Exists("flip")) 47 | assertEquals(t, false, ctx.Exists("bash")) 48 | } 49 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import "net/http" 4 | 5 | type chainHandler func(*Context) http.Handler 6 | type chainMiddleware func(*Context, http.Handler) http.Handler 7 | 8 | type Chain struct { 9 | mws []chainMiddleware 10 | h chainHandler 11 | } 12 | 13 | func New(mws ...chainMiddleware) Chain { 14 | return Chain{mws: mws} 15 | } 16 | 17 | func (c Chain) Append(mws ...chainMiddleware) Chain { 18 | newMws := make([]chainMiddleware, len(c.mws)+len(mws)) 19 | copy(newMws[:len(c.mws)], c.mws) 20 | copy(newMws[len(c.mws):], mws) 21 | c.mws = newMws 22 | return c 23 | } 24 | 25 | func (c Chain) Then(chf func(ctx *Context, w http.ResponseWriter, r *http.Request)) HandlerChain { 26 | c.h = adaptContextHandlerFunc(chf) 27 | return newHandlerChain(c) 28 | } 29 | 30 | func (c Chain) ThenHandler(h http.Handler) HandlerChain { 31 | c.h = adaptHandler(h) 32 | return newHandlerChain(c) 33 | } 34 | 35 | func (c Chain) ThenHandlerFunc(fn func(http.ResponseWriter, *http.Request)) HandlerChain { 36 | c.h = adaptHandlerFunc(fn) 37 | return newHandlerChain(c) 38 | } 39 | 40 | type HandlerChain struct { 41 | context *Context 42 | Chain 43 | } 44 | 45 | func newHandlerChain(c Chain) HandlerChain { 46 | return HandlerChain{context: NewContext(), Chain: c} 47 | } 48 | 49 | func (hc HandlerChain) ServeHTTP(w http.ResponseWriter, r *http.Request) { 50 | // Always take a copy of context (i.e. pointing to a brand new memory location) 51 | ctx := hc.context.copy() 52 | 53 | final := hc.h(ctx) 54 | for i := len(hc.mws) - 1; i >= 0; i-- { 55 | final = hc.mws[i](ctx, final) 56 | } 57 | final.ServeHTTP(w, r) 58 | } 59 | 60 | func Inject(hc HandlerChain, key string, val interface{}) HandlerChain { 61 | hc.context = hc.context.copy().Put(key, val) 62 | return hc 63 | } 64 | 65 | // Adapt third party middleware with the signature 66 | // func(http.Handler) http.Handler into chainMiddleware 67 | func Adapt(fn func(http.Handler) http.Handler) chainMiddleware { 68 | return func(ctx *Context, h http.Handler) http.Handler { 69 | return fn(h) 70 | } 71 | } 72 | 73 | // Adapt http.Handler into a chainHandler 74 | func adaptHandler(h http.Handler) chainHandler { 75 | return func(ctx *Context) http.Handler { 76 | return h 77 | } 78 | } 79 | 80 | // Adapt a function with the signature 81 | // func(http.ResponseWriter, *http.Request) into a chainHandler 82 | func adaptHandlerFunc(fn func(w http.ResponseWriter, r *http.Request)) chainHandler { 83 | return adaptHandler(http.HandlerFunc(fn)) 84 | } 85 | 86 | // Adapt a function with the signature 87 | // func(Context, http.ResponseWriter, *http.Request) into a chainHandler 88 | func adaptContextHandlerFunc(fn func(ctx *Context, w http.ResponseWriter, r *http.Request)) chainHandler { 89 | return func(ctx *Context) http.Handler { 90 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 91 | fn(ctx, w, r) 92 | }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /stack_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | ) 11 | 12 | func assertEquals(t *testing.T, e interface{}, o interface{}) { 13 | if e != o { 14 | t.Errorf("\n...expected = %v\n...obtained = %v", e, o) 15 | } 16 | } 17 | 18 | func serveAndRequest(h http.Handler) string { 19 | ts := httptest.NewServer(h) 20 | defer ts.Close() 21 | res, err := http.Get(ts.URL) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | resBody, err := ioutil.ReadAll(res.Body) 26 | res.Body.Close() 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | return string(resBody) 31 | } 32 | 33 | func bishMiddleware(ctx *Context, next http.Handler) http.Handler { 34 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 35 | ctx.Put("bish", "bash") 36 | fmt.Fprintf(w, "bishMiddleware>") 37 | next.ServeHTTP(w, r) 38 | }) 39 | } 40 | 41 | func flipMiddleware(ctx *Context, next http.Handler) http.Handler { 42 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 43 | fmt.Fprintf(w, "flipMiddleware>") 44 | next.ServeHTTP(w, r) 45 | }) 46 | } 47 | 48 | func wobbleMiddleware(next http.Handler) http.Handler { 49 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 50 | fmt.Fprintf(w, "wobbleMiddleware>") 51 | next.ServeHTTP(w, r) 52 | }) 53 | } 54 | 55 | func bishHandler(ctx *Context, w http.ResponseWriter, r *http.Request) { 56 | val := ctx.Get("bish") 57 | fmt.Fprintf(w, "bishHandler [bish=%v]", val) 58 | } 59 | 60 | func flipHandler(ctx *Context, w http.ResponseWriter, r *http.Request) { 61 | valb := ctx.Get("bish") 62 | valf := ctx.Get("flip") 63 | fmt.Fprintf(w, "flipHandler [bish=%v,flip=%v]", valb, valf) 64 | } 65 | 66 | func TestNew(t *testing.T) { 67 | st := New(bishMiddleware, flipMiddleware).Then(bishHandler) 68 | res := serveAndRequest(st) 69 | assertEquals(t, "bishMiddleware>flipMiddleware>bishHandler [bish=bash]", res) 70 | } 71 | 72 | func TestAppend(t *testing.T) { 73 | st := New(bishMiddleware).Append(flipMiddleware, flipMiddleware).Then(bishHandler) 74 | res := serveAndRequest(st) 75 | assertEquals(t, "bishMiddleware>flipMiddleware>flipMiddleware>bishHandler [bish=bash]", res) 76 | } 77 | 78 | func TestAppendDoesNotMutate(t *testing.T) { 79 | st1 := New(bishMiddleware, flipMiddleware) 80 | st2 := st1.Append(flipMiddleware, flipMiddleware) 81 | res := serveAndRequest(st1.Then(bishHandler)) 82 | assertEquals(t, "bishMiddleware>flipMiddleware>bishHandler [bish=bash]", res) 83 | res = serveAndRequest(st2.Then(bishHandler)) 84 | assertEquals(t, "bishMiddleware>flipMiddleware>flipMiddleware>flipMiddleware>bishHandler [bish=bash]", res) 85 | } 86 | 87 | func TestThen(t *testing.T) { 88 | chf := func(ctx *Context, w http.ResponseWriter, r *http.Request) { 89 | fmt.Fprint(w, "An anonymous ContextHandlerFunc") 90 | } 91 | st := New().Then(chf) 92 | res := serveAndRequest(st) 93 | assertEquals(t, "An anonymous ContextHandlerFunc", res) 94 | } 95 | 96 | func TestThenHandler(t *testing.T) { 97 | st := New().ThenHandler(http.NotFoundHandler()) 98 | res := serveAndRequest(st) 99 | assertEquals(t, "404 page not found\n", res) 100 | } 101 | 102 | func TestThenHandlerFunc(t *testing.T) { 103 | hf := func(w http.ResponseWriter, r *http.Request) { 104 | fmt.Fprint(w, "An anonymous HandlerFunc") 105 | } 106 | st := New().ThenHandlerFunc(hf) 107 | res := serveAndRequest(st) 108 | assertEquals(t, "An anonymous HandlerFunc", res) 109 | } 110 | 111 | func TestMixedMiddleware(t *testing.T) { 112 | st := New(bishMiddleware, Adapt(wobbleMiddleware), flipMiddleware).Then(bishHandler) 113 | res := serveAndRequest(st) 114 | assertEquals(t, "bishMiddleware>wobbleMiddleware>flipMiddleware>bishHandler [bish=bash]", res) 115 | } 116 | 117 | func TestInject(t *testing.T) { 118 | st := New(flipMiddleware).Then(flipHandler) 119 | st2 := Inject(st, "bish", "boop") 120 | 121 | res := serveAndRequest(st2) 122 | assertEquals(t, "flipMiddleware>flipHandler [bish=boop,flip=]", res) 123 | 124 | res = serveAndRequest(st) 125 | assertEquals(t, "flipMiddleware>flipHandler [bish=,flip=]", res) 126 | } 127 | --------------------------------------------------------------------------------