huma icon indicating copy to clipboard operation
huma copied to clipboard

global agnostic middleware

Open mybigman opened this issue 1 year ago • 5 comments

It appears that global middleware does not run without a route? If that is the case you can never terminate early.

Another scenario might be I have a middleware that looks for a specific header and does something with that before passing it down the chain regardless if the a route exists or not.

All other frameworks I have used handle this fine.

Its possible I have overlooked something in the docs?

func main() {
    api := humago.New(router, huma.DefaultConfig("My API", "1.0.0"))
    api.UseMiddleware(heartbeat.New("/ping"))
    addRoutes(api)
    ...
}
// heartbeat.go
func New(path string) func(ctx huma.Context, next func(huma.Context)) {
    return func(ctx huma.Context, next func(huma.Context)) {
        if (ctx.Method() == "GET" || ctx.Method() == "HEAD") && strings.EqualFold(ctx.URL().Path, path) {
            ctx.SetHeader("Content-Type", "text/plain")
            ctx.SetStatus(http.StatusOK)
            ctx.BodyWriter().Write([]byte("."))

            return
        }

        next(ctx)
    }
}

mybigman avatar Feb 21 '24 06:02 mybigman

Yes, right now router-agnostic middlewares are only assigned at operation registration time so are specific to all the operation paths in your API. I think you're right that we should rethink this approach. I'll consider this a bug for now, thanks for reporting!

danielgtaylor avatar Feb 21 '24 15:02 danielgtaylor

The more I look into this the more difficult it seems to be to support, due to the nature of Huma being router-agnostic and the fact that the Huma layer which creates the huma.Context comes after the router-specific middleware has run. I'm struggling to figure out a way this could work.

I think global router-agnostic middleware would require adding a middleware to the underlying router, but Go 1.22 has no middleware functionality built-in so would require wrapping the router's ServeHTTP and users would have to pass that to the server's Listen call (which isn't supported by Fiber). Not to mention if we create the huma.Context there then how does it get stored until the operation handler needs it to run? 🤔 Open to ideas on this one...

In the meantime, your heartbeat example can be accomplished this way without relying on any specific router implementation. The adapter's Handle() method bypasses registering the operation in the OpenAPI and doesn't use any of the middleware:

// Heartbeat registers a simple heartbeat endpoint with a success response.
func Heartbeat(api huma.API, path string) {
	api.Adapter().Handle(&huma.Operation{
		Method: http.MethodGet,
		Path:   path,
	}, func(ctx huma.Context) {
		ctx.SetHeader("Content-Type", "text/plain")
		ctx.SetStatus(http.StatusOK)
		ctx.BodyWriter().Write([]byte("."))
	})
}

// ...

api := humachi.New(mux, huma.DefaultConfig("Greet API", "1.0.0"))
Heartbeat(api, "/ping")

If you do want to use the middleware for this simple call, you can wrap the handler function like this:

api.Adapter().Handle(&huma.Operation{
	Method: http.MethodGet,
	Path:   path,
}, api.Middlewares().Handler(func(ctx huma.Context) {
	ctx.SetHeader("Content-Type", "text/plain")
	ctx.SetStatus(http.StatusOK)
	ctx.BodyWriter().Write([]byte("."))
}))

danielgtaylor avatar Feb 23 '24 16:02 danielgtaylor

Sounds like a not so easy solution, appreciated that you looked into it.

The more Huma gains traction I feel that this is going to be wanted :)

mybigman avatar Feb 24 '24 13:02 mybigman

Hello @mybigman ! I don’t quite understand why this functionality is needed, what global problems will this solve?

does something with that before passing it down the chain regardless if the a route exists or not.

Are we talking about custom 404 responses and etc?

For your specific task, it is easy to create two /ping endpoints with GET and HEAD operations, which will be processed by the same function.

Insei avatar Feb 27 '24 11:02 Insei

The heartbeat probably wasn't a good choice.

As mentioned if I have a middleware that checks headers before any routes are called to do something. This wont work and would have to resort to using the routers middleware function which defeats the router agnostic approach.

mybigman avatar Feb 28 '24 11:02 mybigman