Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Value attached to request context in chi middleware is not available in context in huma operation #197

Closed
JanRuettinger opened this issue Jan 3, 2024 · 3 comments

Comments

@JanRuettinger
Copy link

JanRuettinger commented Jan 3, 2024

I am currently porting over a code from a pure chi router to huma + chi. I have a middleware that checks the Bearer token in the request header and fetches the user object from the db and then attaches it to the request context to make it available in all downstream functions. While porting the code over I ran into an issue. When I attach a value to the context of a request in a chi middleware, the value is not available in the context of the huma Operation.

My mental framework is as following:
Request -> chi middleware -> attaches value to Request context -> Request context is copied and made available in each huma Operation

That mental framework is not accurate. I'd like to understand where it breaks and how to best fix my issue of making a user object available within a huma Operation.

Code example below:

func (app *application) authenticate(next http.Handler) http.Handler {

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
               
                // simplified
	        user := getUser(r)
		r = contextSetAuthenticatedUser(r, user) // attach user to context of request!
		next.ServeHTTP(w, r)
	})

}


router := chi.NewMux()
router.Use(app.authenticate)
config := huma.DefaultConfig("Dumbledore API", "0.0.1")
api := humachi.New(router, config)


huma.Register(api, huma.Operation{
	OperationID: "dummy-route",
	Summary:     "dummy route",
	Method:      http.MethodGet,
	Path:        "/dummyroute",
}, func(ctx context.Context, i *struct{}) (*DummyRouteResponse, error) {

        // ISSUE: value is not available here
	user, ok := ctx.Value("user).(*database.User)
	if !ok {
		app.logger.Info("no authenticated user found in context")
		return nil, fmt.Errorf("no authenticated user found in context")
	}

	       ...
		
     })

return router
}
@danielgtaylor
Copy link
Owner

In Huma, the operation handler dependencies need to be a bit more explicit, so you don't get access to the context vars implicitly. You can do this using a resolver and including that in your input struct via composition for the operation handler, then explicitly accessing it via e.g. input.User:

type AuthHeader struct {
	User string
}

func (a *AuthHeader) Resolve(ctx huma.Context) []error {
	if v := ctx.Context().Value("user"); v != nil {
		a.User = v.(string)
	}
	return nil
}

func main() {
	router := chi.NewMux()

	router.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			r = r.WithContext(context.WithValue(r.Context(), "user", "foo"))
			next.ServeHTTP(w, r)
		})
	})

	// Register GET /greeting/{name}
	huma.Register(api, huma.Operation{
		OperationID: "get-greeting",
		Summary:     "Get a greeting",
		Method:      http.MethodGet,
		Path:        "/greeting/{name}",
	}, func(ctx context.Context, input *struct {
		AuthHeader
		GreetingInput
	}) (*GreetingOutput, error) {
		fmt.Println("User is", input.User)
		resp := &GreetingOutput{}
		resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
		return resp, nil
	})
}

While it's not as "nice" as just pulling things from the request context, this was done on purpose to make all the inputs/outputs of an operation explicitly defined and limit the magic that can confuse people and lead to mistakes.

@JanRuettinger
Copy link
Author

Great answer!

Follow up question: how can I access a dependency, e.g. database connection, in the resolver function?

I could potentially add the dependency to the request context in middleware call before but this seems overly complex.
And it looks like the request context can't be easily manipulated from the huma.Context since there is no setter method.

@danielgtaylor
Copy link
Owner

Follow up question: how can I access a dependency, e.g. database connection, in the resolver function?

You can't directly access dependencies that aren't in scope in a resolver. It's recommended you do this in the handler function or using some middleware that can be initialized with the dependency before use.

I could potentially add the dependency to the request context in middleware call before but this seems overly complex.
And it looks like the request context can't be easily manipulated from the huma.Context since there is no setter method.

Right, you can't update the context from a resolver, but you can set fields on the resolved struct for use by the operation handler. The User string is an example of that above. The middleware above could take in a reference to the DB which will be set up at server start based on some URL/user/pass in the environment, then when a request comes in with an Authorization header you can parse/validate it and use that DB to fetch the user and put it in the context.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants