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

Best way to pass information between middlewares and then to the operation #224

Closed
servernoj opened this issue Feb 1, 2024 · 3 comments
Closed

Comments

@servernoj
Copy link

I came from Gin, where gin.Context.Set and gin.Context.Get methods were used to pass arbitrary data between middlewares up to the final handler. I understand that Huma is router agnostic, and such a thing as context value passing is not [probably] implemented because not all routers support it.

I can see that middleware can pass along huma.Context, which in turn has a getter for context.Context (and I also see that operation handler has access to context.Context), but I cannot think of a way to replace it inside middleware with another context.Context generated by context.WithValue().

My final goal is to use this feature to be able to parse a JWT token from the Authorization header in a middleware and set userId (or even user) for all attached operations. This is a shared logic that I don't want to implement on the operation level.

An alternative would be to let input structs to implement huma.Resolver as shown in https://huma.rocks/features/request-resolvers/#request-resolvers and dedicate a field on the input struct to carry over that userId for the assigned operation... but this is not different than having that logic implemented by the operation itself (other operations cannot benefit from this).

I am afraid I am thinking in the wrong terms and/or trying to move against the stream. What would be the idiomatic way of handling auth middleware that I have just described?

@servernoj
Copy link
Author

I find this issue related to #197, and the proposed solution would be to use "native" Gin router to modify the request's context via context.WithValue() and then use a sharable struct that implements huma.Resolver to extract the value and store it in this custom struct. Then that struct would be embedded to the operation input to deliver the value.

I guess I was missing the idea of embedding a sharable custom struct into inputs of all operations that need it.

I am OK with this approach, but in case there is a better way I'd like to hear it.

Thank you for the great work!

@servernoj servernoj changed the title Best way to pass information between middlewares Best way to pass information between middlewares and then to the operation Feb 1, 2024
@danielgtaylor
Copy link
Owner

@servernoj there seem to be a couple of questions here. First, is it possible to set a custom value in the context in a router-agnostic middleware? Since huma.Context is an interface, it is possible to wrap and replace the .Context() method like this:

// Temporarily rename the type to not clash with the method name.
type humaContext huma.Context

type MyContext struct {
	humaContext
}

func (c *MyContext) Context() context.Context {
	// Set some custom value into the context and return it.
	return context.WithValue(c.humaContext.Context(), "key", "value")
}

func MyMiddleware(ctx huma.Context, next func(huma.Context)) {
	// Call the next middleware in the chain with the wrapped context.
	next(&MyContext{ctx})
}

This is a fairly common pattern in Go so may already feel familiar. Your custom context can change/update any behavior you like to enable setting or passing whatever data you might need.

Second, how best to access data from the context in an operation handler function? Your proposal of using a shared struct with a resolver as input to the operations that need access to it is exactly what I would have suggested doing. For example something like this:

type UserInfo struct {
	Username string
}

func (u *UserInfo) Resolve(ctx huma.Context) []error {
	// Get the user info from the context.
	u.Username = ctx.Context().Value("username").(string)
	return nil
}

type UserResponse struct {
	Body struct {
		Name string `json:"name"`
	}
}

func later(api huma.API) {
	huma.Register(api, huma.Operation{
		OperationID: "get-user",
		Method:      http.MethodGet,
		Path:        "/user",
	}, func(ctx context.Context, input *struct {
		UserInfo
	}) (*UserResponse, error) {
		resp := &UserResponse{}
		resp.Body.Name = input.Username
		return resp, nil
	})
}

Now, if you don't like the idea of using a resolver, another idea might be to have a router-specific middleware insert a new header like InternalUsername and then doing the following, since the middleware runs before the input structs are processed:

type UserInfo struct {
	Username string	`header:"InternalUsername" hidden:"true"`
}

@servernoj
Copy link
Author

Thank you, @danielgtaylor. I need a bit of time to digest your answer to my first question. But the second one is all clear.

I'm still [slightly] confused about where huma.Context is treated as an interface and where it is treated as an instance/object. I think it has something to do with humagin adapter where a struct implementing huma.Context is created and then passed into middlewares, and so on, but I'm not sure I get it right.

I am closing the issue as you have addressed all my questions. Thank you.

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