-
Notifications
You must be signed in to change notification settings - Fork 58
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
Add middleware support #71
Conversation
group.go
Outdated
@@ -6,9 +6,19 @@ import ( | |||
"strings" | |||
) | |||
|
|||
type Middleware func(next HandlerFunc) HandlerFunc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With latest golang's version, there is some traction to define middlewares in router-agnostic way, ie: func(http.Handler) http.Handler
Maybe, helper function to wrap middleware(s) using standard http package would be useful. For example echo#WrapMiddleware.
Chi router even uses middleware definition based on standard http package directly chi#Mux.Use.
Existing middlewares:
- https://godoc.org/github.com/rs/cors#Cors.Handler (it's method, but can be used as
func(http.Handler) http.Handler
), - https://godoc.org/github.com/rs/formjson#Handler,
- https://www.godoc.org/github.com/philippgille/ln-paywall/wall#NewHandlerMiddleware
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am all for it, but passing params
in a fast way without allocation is the problem, e.g. you could
ctx = ctx.WithValue("params", params) // wastes some CPU (and allocates?)
req = req.WithContext(ctx) // allocates
req.Context().Value("params").(map[string]string) // and again
It probably does not matter in practice (except in benchmarks tournament) but it is also trivial to use standard middlewares with current design too:
router.Use(func(next HandlerFunc) HandlerFunc {
h := cors.New(...)
return func(w http.ResponseWriter, r *http.Request, params map[string]string) {
h.HandlerFunc(w, r)
next(w, r, params)
}
})
httptreemux can even provide an adapter so the code above becomes
h := cors.New(...)
router.Use(httptreemux.Adapter(h.HandlerFunc))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or even
h := cors.New(...)
router.UseHandlerFunc(h.HandlerFunc)
I will add it - good idea :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am all for it, but passing params in a fast way without allocation is the problem, e.g. you could...
Makes sense. Maybe for httptreemux, WrapMiddleware(m func(http.Handler) http.Handler) Middleware
makes more sense.
Or even
I meant to stick to http.Handler, which is more widely used, as http.HandlerFunc is just an adapter for functions:
h := cors.New(...)
router.Use(httptreemux.WrapMiddleware(h.Handler))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now I've added only UseHandlerFunc
since it is trivial and without "side effects".
It is possible to support Handler(h http.Handler) http.Handler
too but there are 2 possibilities:
- don't pass
params
further in chain which I guess is not a good option unless params are inreq.Context
already - allocate a closure for
params
on each request to preserveparams
- save params in
req.Context
if they are not there already, but then we pay the cost of usingcontext
andrequest.WIthContext
without being compatible with Go ecosystem
My preference would be 2.) (and drop ContextGroup
support - only half-joking) :) Or drop Group
and use ContextGroup
as the only option to have max compatibility with Go ecosystem (which makes much more sense). Then we don't need these adapters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would go for maximum compatibility with Go ecosystem. However, @dimfeld should call shots.
Also, although function definition is convenient, interface based types allow to provide much more optional functionality, and not demand these extra functionalities to be implemented.
For example it is possible to list/print registered middlewares in user-friendly way, using fmt#Stringer: if asStringer, ok := middleware.(fmt.Stringer); ok { ... } else { ... }
. Or, via type MiddlewareDescriber interface { DescribeMiddleware() string }
This could allow to produce debug output after initialization of router, and list all registered routes and used middlewares in user-friendly way.
handler: next, | ||
params: params, | ||
} | ||
middleware(nextHandler).ServeHTTP(w, r) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard to tell how good this will work in practice if middleware does some heavy initialization on middleware(nextHandler)
call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO, middleware initialization should be lightweight, ie:
package middleware
type Middleware struct {...}
func New(...) *Middleware { /* initialize here */ }
type handler {
m *Middleware
next http.Handler
}
// implement handler as http.Handler
func (m *Middleware) Handler (next func(http.Handler) http.Handler {
return handler{m: m, next: next}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One big reason for it to be lightweight is, that middleware will be used with many routes (different http.Handler
s), and therefore it doesn't make sense to do heavy initialization for each route separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though,... this is internal implementation. The primary focus should be on public/exported API, which should stay same, once it's released.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I follow. I mean that some existing 3rd party middlewares may do some initialization on middleware(nextHandler)
call since most routers call middleware(nextHandler)
once - not for every route call. We can't do that because we need to capture params
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... call since most routers call
middleware(nextHandler)
once - not for every route call...
Not once per handled request, but once per route handler registration, as each registered route has got different handler.
With tens of registered routes, it doesn't make sense to have heavy weight initialization there, as it would mean for middleware to have heavy-weight handler wrapper,... therefore probably lots of data duplication (unnecessary memory consumption).
@vmihailenco is it still work-in-progress, or ready for merge / review by @dimfeld ? Actually, with this feature,... I might even start considering to switch from chi router to httptreemux. It was probably the most important reason, why I chose chi. |
Yes, it should be ready for review. |
Thanks! I think this looks pretty good. I added a couple tests for params that I'll commit separately. I agree that it would be nice to have a solution for http.Handler middleware that didn't involve calling the middleware function again for each request. I'm not entirely sure how to do that though without using Context. Perhaps that's ok, and we can put UseHandler in another file that only compiles under go 1.7+ where Context is available. Presumably anyone still on an older version of Go won't be using these new features anyway. Any thoughts on this? |
Updates #69