-2

I'm writing a more or less simple web application using go that provides a rest api. When a request comes in, I want to store the id of the user, which is part of the api token, temporarily inside the requests context. After reading some articles and the docs I'm still confused how to make sure that the values, that one attaches to the context using context.WithValue(), can be used without type assertion, but a struct of sorts instead.

This is what I've come up with so far:

// RequestContext contains the application-specific information that are carried around in a request.
type RequestContext interface {
    context.Context
    // UserID returns the ID of the user for the current request
    UserID() uuid.UUID
    // SetUserID sets the ID of the currently authenticated user
    SetUserID(id uuid.UUID)
}

type requestContext struct {
    context.Context           // the golang context
    now             time.Time // the time when the request is being processed
    userID          uuid.UUID // an ID identifying the current user
}

func (ctx *requestContext) UserID() uuid.UUID {
    return ctx.userID
}

func (ctx *requestContext) SetUserID(id uuid.UUID) {
    ctx.userID = id
}

func (ctx *requestContext) Now() time.Time {
    return ctx.now
}

// NewRequestContext creates a new RequestContext with the current request information.
func NewRequestContext(now time.Time, r *http.Request) RequestContext {
    return &requestContext{
        Context: r.Context(),
        now:     now,
    }
}

// RequestContextHandler is a middleware that sets up a new RequestContext and attaches it to the current request.
func RequestContextHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        now := time.Now()
        next.ServeHTTP(w, r.WithContext(NewRequestContext(now, r)))
    })
}

I would like to know how I can access the SetUserID() and UserID() functions of the request context inside a handler or if there is an alternative, similar approach.

Tom
  • 4,070
  • 4
  • 22
  • 50
  • 1
    Context is a great way to manage cancellation and a terrible way to pass data. You'd be better off just making `RequestContext` a regular struct (not trying to make it `context.Context`-compatible) and making your handlers methods on that struct. That way you can fill in whatever fields you want, type-safe, and then call the handler method, which can access those fields on its receiver. – Adrian Dec 05 '19 at 15:33
  • Very well put. Thanks :) – Tom Dec 05 '19 at 17:36

2 Answers2

4

To use what you built, you have to use type assertions:

rctx:=request.Context().(RequestContext)

This will break if you have a middleware that wraps the context the same way you did.

An alternative is using the base context, and adding helpers with a private key to access values:

type reqKey int

const key reqKey=iota

type RequestData struct {
   UserID uuid.UUID
   Now time.Time
}

func ContextWithRequest(ctx context.Context,req requestData) context.Context {
  return context.WithValue(ctx,key,req)
}

func GetRequest(ctx context) RequestData {
  x:=ctx.Value(key)
  if x!=nil {
     return x.(RequestData)
  }
  return RequestData{}
}

Of course, if you want to change the RequestData, you need to add a pointer to the context.

Burak Serdar
  • 46,455
  • 3
  • 40
  • 59
0

This is what I'm using in my application now. I use simple settters and getters to retrieve values from the context.

import (
    "context"
    "github.com/google/uuid"
)

const userIdContextKey = "user_id" // type is uuid

func addUserIdToContext(ctx context.Context, user uuid.UUID) context.Context {
    return context.WithValue(ctx, UserIdContextKey, user)
}

func userIDFromContext(ctx context.Context) uuid.UUID {
    return ctx.Value(UserIdContextKey).(uuid.UUID)
}

Then in a middleware, where I've retrieved the userId, I add the value to the context, by passing the context and user id to the addUserIdToContext() function.

ctx := addUserIdToContext(r.Context(), userId)

When I want to retrieve the user from the context, I just have to call userIDFromContext(ctx) and pass it the requests context.

func CreateResource(w http.ResponseWriter, r *http.Request) {
    createdBy := userIDFromContext(r.Context())
    // do something with createdBy
}

I'm aware that this isn't the most elegant or even type safe solution, but hey, for what I'm trying to build it works just fine.

Tom
  • 4,070
  • 4
  • 22
  • 50