gohttprequesthttproutergo-context

Context value is nil when getting it with unexported struct key in Go HTTP handlers


Any help here is appreciated! I'm sure that I'm missing something really basic.

The problem I have is I am trying to get a value out of context in a demo web application, and I'm receiving the error:

2021/04/11 11:35:54 http: panic serving [::1]:60769: interface conversion: interface {} is nil, not []string

In my main function I'm setting the context with the following:

package main

type ctxKey struct{}

func someHttpHandleFunc() {
  // .....
  ctx := context.WithValue(r.Context, ctxKey{}, matches[1:])
  route.handle(w, r.WithContext(ctx))
}

Then in my handler, I have the following:

package some_package

type ctxKey struct{}
func getField(r *http.Request, index int) string {
    fields := r.Context().Value(ctxKey{}).([]string)
    return fields[index]
}

I know that I'm missing something simple because if I try the above code and put my getField() function within the package main everything works.

For reference, this is a learning exercise, I'm trying to teach myself Go routing. I do know that there are routing packages available - but my goal is to learn. I'm trying my best to follow along with Different approaches to HTTP routing in Go. I have also read through Pitfalls of context values and how to avoid or mitigate them in Go. The latter seems to directly address the problem I'm having, but I can't seem to figure out how to solve it based on what is there.


Solution

  • Defined struct types defined in different packages are different.

    package main
    
    type ctxKey struct{}
    

    is not the same type as

    package some_package
    
    type ctxKey struct{}
    

    To make it intuitively clearer, think that if you were to reference these types from a third package — let's pretend the types were exported — you would have to import the corresponding package and use the appropriate selector:

    package baz
    
    import (
       "myproject/foo"
       "myproject/some_package"
    )
    
    func doBaz() {
        foo := foo.CtxKey{}
        bar := some_package.CtxKey{}
    }
    

    Now, the implementation of Value(key interface{}) uses the comparison operator == to determine whether the supplied key matches:

    func (c *valueCtx) Value(key interface{}) interface{} {
        if c.key == key {
            return c.val
        }
        // then it checks if the key exists on the parent
        return c.Context.Value(key)
    }
    

    Which implies that the types also must match. From the specs, comparison operators

    In any comparison, the first operand must be assignable to the type of the second operand, or vice versa.

    And clearly in your example ctxKey struct{} declared in main is not assignable to ctxKey struct{} declared in some_package, and vice-versa, because their types differ.

    To solve your error, make sure the keys used when setting and getting the context value are the same type. The best way, also to ensure proper encapsulation, would probably be to set and get the context value from the same package:

    package some_ctx_helper_pkg
    
    // unexported, to ensure encapsulation
    type ctxKey struct{}
    
    func Set(ctx context.Context, value interface{}) context.Context {
        return context.WithValue(ctx, ctxKey{}, value)
    }
    
    func Get(ctx context.Context) interface{} {
        return ctx.Value(ctxKey{})
    }