I am trying to understand how the context introduced in Golang 1.7 works and what would be the appropriate way to pass it to middleware and to a HandlerFunc
. Should the context get initialized in the main function and passed to the checkAuth
function then? And how to pass it to Hanlder
and the ServeHTTP
function?
I read Go concurrency patterns and How to use Context but I struggle to adapt those patterns to my code.
func checkAuth(authToken string) util.Middleware {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Auth") != authToken {
util.SendError(w, "...", http.StatusForbidden, false)
return
}
h.ServeHTTP(w, r)
})
}
}
// Handler is a struct
type Handler struct {
...
...
}
// ServeHTTP is the handler response to an HTTP request
func (h *HandlerW) ServeHTTP(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
// decode request / context and get params
var p params
err := decoder.Decode(&p)
if err != nil {
...
return
}
// perform GET request and pass context
...
}
func main() {
router := mux.NewRouter()
// How to pass context to authCheck?
authToken, ok := getAuthToken()
if !ok {
panic("...")
}
authCheck := checkAuth(authToken)
// initialize middleware handlers
h := Handler{
...
}
// chain middleware handlers and pass context
router.Handle("/hello", util.UseMiddleware(authCheck, Handler, ...))
}
In this blog, we cover Golang contexts with use cases, examples, and in-depth theory. Golang Context is a tool that is used to share request-scoped data, cancellation signals, and timeouts or deadlines across API layers or processes in a program. It is one of the most important tools while working with concurrent programming in Go.
Similarly, JWT (JSON Web Tokens) are turning into an increasingly popular way of authenticating users. In this post I shall go over how to create an authentication middleware for Golang that can restrict certain parts of your web app to require authentication.
Middleware is a term used in many different ways within software development, but we’re referring to it as a way to wrap requests and responses in simple abstracted functions which can be applied to some routes easily.
Depending on if you want to share your middleware with the rest of the world, you may adopt a more standard interface. For example, nosurf uses its own context based on the same concept as gorilla/context and has a standard middleware interface. You can use it in almost any project built with any framework without issues.
If you look at the first example at that Go Concurrency Patterns blog post, you'll notice that they're "deriving" their contexts from the Background
context. That, combined with the Context
and WithContext
methods on your Request
object, gives you what you need.
I just figured this out (and it wasn't my first run at reading those docs); when you "derive" a context, you're making another one with one change. I was already wrapping the http.Handler
(actually using httprouter.Handle
). The cool thing about Request.Context
is that it never returns nil
; if no other context has been created, it returns the background context.
To specify a timeout, within your handler (just above your "// perform GET request" comment), you can do something like:
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(60*time.Second))
defer cancel()
r = r.WithContext(ctx)
The first line creates the context and gives you the cancel hook, which you defer; any derived contexts (AKA the ones to which you add your variables) will be cancelled when this deferred call (line 2) is executed, once the request has been served. Finally, line 3 replaces the request, which now contains your updated context.
In your authorization checker, once you have established that the user is valid, you can add the user's information to the context before calling ServeHTTP
. Keys for the context can't use built-in types, but you can create a new type that's simply an alias to the built-in type. It's a good idea to define constants for your key values as well. An example:
type ContextKey string
const ContextUserKey ContextKey = "user"
// Then, just above your call to ServeHTTP...
ctx := context.WithValue(r.Context(), ContextUserKey, "theuser")
h.ServeHTTP(w, r.WithContext(ctx))
This passes the now-twice-derived context (that now has the timeout and the user ID) to the handler.
Finally, the last piece of the puzzle - how to get the user ID from within the handler. This is the most straightforward part; we simply use the Value
method on the value returned from the request's Context
method. The type is interface{}
, so you'll need a type assertion if you want to treat it as a string (as this example does).
user := r.Context().Value(ContextUserKey)
doSomethingForThisUser(user.(string))
You're not limited to one change per method; as long as you keep deriving the same context, it'll all get cleaned up once the request has been served, when the initially derived context (the one, in this example, where we specified the timeout) is cancelled when the deferred cancel()
call fires.
You can improve Daniels solution by adding a funtion to retrieve the value from the context in a typesafe manner:
type ContextKey string
const ContextUserKey ContextKey = "user"
func UserFromContext(ctx context.Context) string {
return ctx.Value(ContextUserKey).(string)
}
// Then, just above your call to ServeHTTP...
ctx := context.WithValue(r.Context(), userKey, "theuser")
h.ServeHTTP(w, r.WithContext(ctx))
The handler does not have to cast the type and it even does not need to know the context key.
If anyone trying to add context value in the handler function and handles it in the middleware.
net/http
provides Request.Clone
and Request.WithContext
methods to modify the request context but both return with a new request pointer which can not change the original *http.Request
instead of doing so, you can try this:
func someHandler(w http.ResponseWriter, r *http.Request){
ctx := r.Context()
req := r.WithContext(context.WithValue(ctx, "key", "val"))
*r = *req
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With