Not sure how to structure your Go web application? My book Let's Go is a clear, concise and easy-to-follow guide which packs in everything you need to know about best practices, project structure and practical code patterns. Take a look!

Published on:
Filed under: golang open-source

Context-Aware Handler Chains in Go (using Stack)

As of Go 1.7 the context package is now part of Go's standard library and you should use that to pass data between your handlers. Joe Shaw has made a good tutorial here. If you're using a Go version < 1.7 then the information below may still be useful.

I've written a package for chaining context-aware handlers in Go, called Stack (view it on GitHub). It was heavily inspired by Alice.

What do you mean by 'context-aware'?

If you're using a middleware pattern to process HTTP requests in Go, you may want to share some data or context between middleware handlers and your application handlers. For example you might want to:

  • Use some middleware to create a CRSF token, and later render the token to a template in your application handler. Or perhaps...
  • Authenticate a user in one middleware handler, and then pass the user details to a second middleware handler which checks if the user is authorised to access the resource.

There are a few packages that can help with this. Matt Silverlock has written a good article about some of the different approaches and tools – I won't rehash it here, instead I recommend giving it a read.

Why make another package?

Because none of the existing tools seemed ideal – at least to me. gorilla/context is simple and very flexible, but relies on a global context map and you remembering to clear the context after each request. (It's still my favourite though). goji provides request-scoped context, which is good, but it's part of a larger package and ties you into using the Goji router. The same is true of gocraft/web, which also relies on reflection tricks under the hood that I struggle to wrap my head around.

I realised that the only time you need to worry about context is when you're chaining handlers together. So I looked at my favorite tool for chaining handlers, Alice, and began adapting that to create Stack.

I wanted the package to:

  • Do a simple job, and then get out of the way.
  • Provide a request-scoped context map.
  • Let you create stackable, reusable, handler chains in the Alice style.
  • Be as type-safe at compile time as it possibly could be.
  • Be simple to understand and non-magic.
  • Operate nicely with existing standards. In particular:
    • The handler chain must satisfy the http.Handler interface, so it can be used with the http.DefaultServeMux.
    • It should be compatible with the func(http.Handler) http.Handler pattern commonly used by third-party middleware packages.

The full documentation for Stack is here, but here's a quick example of how to use it:

File: main.go
package main

import (
    "fmt"
    "github.com/alexedwards/stack"
    "github.com/goji/httpauth"
    "net/http"
)

func main() {
    // Setup goji/httpauth, some third-party middleware
    authenticate := stack.Middleware(httpauth.SimpleBasicAuth("user", "pass"))

    // Create a handler chain and register it with the DefaultServeMux
    http.Handle("/", stack.New(authenticate, tokenMiddleware).Then(tokenHandler))
    http.ListenAndServe(":3000", nil)
}

func tokenMiddleware(ctx stack.Context, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Add a value to Context with the key 'token'
        ctx["token"] = "c9e452805dee5044ba520198628abcaa"
        next.ServeHTTP(w, r)
    })
}

func tokenHandler(ctx stack.Context) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Retrieve the token from Context and print it
        fmt.Fprintf(w, "Token is: %s", ctx["token"])
    })
}
$ curl -i user:[email protected]:3000
HTTP/1.1 200 OK
Content-Length: 41
Content-Type: text/plain; charset=utf-8

Token is: c9e452805dee5044ba520198628abcaa
$ curl -i user:[email protected]:3000
HTTP/1.1 401 Unauthorized
Content-Length: 13
Content-Type: text/plain; charset=utf-8
Www-Authenticate: Basic realm="Restricted"

Unauthorized