7

Context-Aware Handler Chains in Go (using Stack)

 3 years ago
source link: https://www.alexedwards.net/blog/handler-chains-using-stack
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Context-Aware Handler Chains in Go (using Stack)
cover-sm.png

Context-Aware Handler Chains in Go (using Stack)

Posted on: 3rd December 2014 Filed under: golang open-source

Note: 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:pass@localhost:3000
HTTP/1.1 200 OK
Content-Length: 41
Content-Type: text/plain; charset=utf-8

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

Unauthorized

If you enjoyed this blog post, don't forget to check out my new book about how to build professional web applications with Go!

Follow me on Twitter @ajmedwards.

All code snippets in this post are free to use under the MIT Licence.

© Alex Edwards 2013-2020

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK