mux, middleware, and subrouters

2,108 views
Skip to first unread message

James Ribe

unread,
Apr 27, 2015, 7:34:50 PM4/27/15
to goril...@googlegroups.com
Hey all,

Long read ahead that culminates in a feature proposal:

I've been struggling to write straightforward code when using subrouters and/or middleware.

For example, I want to build a router that handles the following paths
  • /path1
  • /path2/inner1
  • /path2/inner2
For code organization reasons, I want /path2's router to be created in a different location. I can implement this in a few ways, but none of them are great.

Technique 1 (init the subrouter):
func NewRouter() *mux.Router {
r := mux.NewRouter()
r.Handle("/path1", myHandler)
inner.InitRouter(r.PathPrefix("/path2").Subrouter())
}

// inner.go
func InitRouter(r *mux.Router) *mux.Router {
r.Handle("/inner1", innerHandler)
r.Handle("/inner2", innerHandler2)
return r
}

This one sucks because:
  • it breaks the pattern of each router call being a method chain ("r.This().That().OtherThing()")
  • building a standalone router looks dumb ("InitRouter(mux.NewRouter())")
Technique 2 (injected prefix subrouter)
func NewRouter() *mux.Router {
r := mux.NewRouter()
r.Handle("/path1", myHandler)
r.PathPrefix("/path2").Handler(inner.NewRouter("/path2"))
}

// inner.go
func NewRouter(prefix string) *mux.Router {
r := mux.NewRouter().PathPrefix(prefix).Subrouter()
r.Handle("/inner1", innerHandler)
r.Handle("/inner2", innerHandler2)
return r
}

This one sucks because:
  • it adds boilerplate to your routers
  • it forces you to repeat yourself with the prefix name
  • nesting deeper than 1 level requires a call to path.Join()
  • the prefix get checked twice
Technique 3 (injected prefix join hell):
func NewRouter() *mux.Router {
r := mux.NewRouter()
r.Handle("/path1", myHandler)
r.PathPrefix("/path2").Handler(inner.NewRouter("/path2"))
}

// inner.go
func NewRouter(prefix string) *mux.Router {
r := mux.NewRouter()
r.Handle(path.Join(prefix, "/inner1"), innerHandler)
r.Handle(path.Join(prefix, "/inner2"), innerHandler2)
return r
}

This one sucks because:
  • calling path.Join() on every path? really?
  • again, needs to call path.Join() to nest deeper than 1 level
Technique 4 (hard-coded prefix):
func NewRouter() *mux.Router {
r := mux.NewRouter()
r.Handle("/path1", myHandler)
r.PathPrefix("/path2").Handler(inner.NewRouter())
}

// inner.go
func NewInnerRouter() *mux.Router {
r := mux.NewRouter()
r.Handle("/path2/inner1", innerHandler)
r.Handle("/path2/inner2", innerHandler2)
return r
}

This one sucks because:
  • you're needlessly repeating the path prefixes everywhere (incredibly error-prone when authoring and refactoring)

These all work, but they get ugly when you throw middleware in the mix.

The common way to use middleware (in this case, Negroni) with mux looks like:
func NewRouter() *mux.Router {
// real router                     // i / c // i - initialization order, c - composition order
r := mux.NewRouter()               // *   ^
r.Handle("/path1", myHandler)      // |   |
r.Handle("/path2", myOtherHandler) // V   |
                                  //     |
// middleware system               //     |
n := negroni.New()                 // *   |
n.Use(authMiddleware.New())        // |   |
n.UseHandler(r)                    // V   |
                                  //     |
// wrapper router                  //     |
outer := mux.NewRouter()           // *   |
outer.PathPrefix("/").Handler(n)   // V   |
                                  //     |
return outer                       //     *
}
This works, but it's pretty ugly code. I'm using middleware and mux because I want to reduce boilerplate and make my code more straightforward, but now my actual router is nested 2 layers deep. Also, this code is unintuitive because each object's code block reads top-to-bottom whereas the nesting order reads bottom-to-top.

Try integrating middleware with nested routers and suddenly we have a problem. Technique 1 doesn't work at all because it loses the prefix and techniques 2-4 work, but become much more difficult to read when doing real work.

To improve my quality of life, I'm creating a fork of mux with the following new functions (still working on it now, will add to this thread when ready):
// copies the router, makes it a subrouter on the Route, returns the new Router
func (*Route) SubrouterFrom(*Router) *Router

// negroni-style middleware handling
type Middleware interface {
        ServeHTTP(http.ResponseWriter, *http.Request, http.HandlerFunc)
}

func (*Router) UseMiddleware(Middleware)
func (*Router) UseMiddlewareFunc(func(http.ResponseWriter, *http.Request, http.HandlerFunc))
func (*Router) UseMiddlewareHandler(http.Handler)
func (*Router) UseMiddlewareHandlerFunc(func(http.ResponseWriter, *http.Request))

Let's look at some example code with nested routers and middleware. We'll use this scheme:
  • /path1 - recovery and logging middleware
  • /path2 - authorization middleware
    • /inner1
    • /inner2
    • /deepPath - database middleware
      • /deep1
Using mux and negroni as-is, it looks like:
func NewRouter() *mux.Router {
    r := mux.NewRouter()
    r.Handle("/path1", myHandler)
    r.PathPrefix("/path2").Handler(inner.NewRouter("/path2"))

    n := negroni.New()
    n.Use(recoveryMiddleware.New())
    n.Use(loggingMiddleware.New())
    n.UseHandler(r)

    outer := mux.NewRouter()
    outer.PathPrefix("/").Handler(n)

    return outer
}

// inner.go
func NewRouter(prefix string) *mux.Router {
    r := mux.NewRouter().PathPrefix(prefix).Subrouter()
    r.Handle("/inner1", innerHandler1)
    r.Handle("/inner2", innerHandler2)
    r.PathPrefix("/deepPath").Handler(deep.NewRouter(path.Join(prefix, "/deepPath")))
    
    n := negroni.New()
    n.Use(AuthMiddleware.New())
    n.UseHandler(r)
    
    outer := mux.NewRouter()
    outer.PathPrefix("/").Handler(n)
    
    return outer
}

// deep.go
func NewRouter(prefix string) *mux.Router {
    r := mux.NewRouter().PathPrefix(prefix).Subrouter()
    r.Handler("/deep1", deepHandler1)
    
    n := negroni.New()
    n.Use(DatabaseMiddleware.New())
    n.UseHandler(r)
    
    outer := mux.NewRouter()
    outer.PathPrefix("/").Handler(n)
    
    return outer
}

Using mux with my proposed features, the same code looks like:
func NewRouter() *mux.Router {
    r := mux.NewRouter()

    r.UseMiddleware(recoveryMiddleware.New())
    r.UseMiddleware(loggingMiddleware.New())

    r.Handle("/path1", myHandler)
    r.PathPrefix("/path2").SubrouterFrom(inner.NewRouter())

    return r
}

// inner.go
func NewRouter() *mux.Router {
    r := mux.NewRouter()

    r.UseMiddleware(authMiddleware.New())

    r.Handle("/inner1", innerHandler1)
    r.Handle("/inner2", innerHandler2)
    r.PathPrefix("/deepPath").SubrouterFrom(deep.NewRouter())

    return r
}

// deep.go
func NewRouter() *mux.Router {
    r := mux.NewRouter()

    r.UseMiddleware(databaseMiddleware.New())

    r.Handler("/deep1", deepHandler1)
    
    return r
}
I like this code a whole lot more! My inner router code never needs to be concerned with what's going on one level up and the middleware handling is easy to understand--each router has its own If I need be, I could even embed other well-behaved routers from other applications under arbitrary paths in my application!

Does this use case make sense to anyone else? Is there a strong argument against adding this functionality to mux once it's stable?

MIhail Rbk

unread,
Feb 7, 2016, 3:03:05 PM2/7/16
to Gorilla web toolkit
I cant find a function SubrouterFrom in gorilla mux,  how i can repeat this expamle just vith subrouter and handle()??  help me please i really need it.
вторник, 28 апреля 2015 г., 5:34:50 UTC+6 пользователь James Ribe написал:

Matt Silverlock

unread,
Feb 8, 2016, 10:06:02 AM2/8/16
to Gorilla web toolkit
SubRouter from was a proposed feature; it does not currently exist in gorilla/mux.

MIhail Rbk

unread,
Feb 8, 2016, 11:07:04 AM2/8/16
to Gorilla web toolkit
okey, but my qestion still waiting answer)

понедельник, 8 февраля 2016 г., 21:06:02 UTC+6 пользователь Matt Silverlock написал:

James Ribe

unread,
Feb 8, 2016, 7:56:32 PM2/8/16
to Gorilla web toolkit
I implemented those features on my fork at https://github.com/Manbeardo/mux, but it hasn't been through a code review and I haven't kept it up to date with gorilla/mux, so no guarantees.

MIhail Rbk

unread,
Feb 8, 2016, 11:30:55 PM2/8/16
to Gorilla web toolkit
Lets repeat my qestion, how a can repeat your expamle( without register midllware, just routes in many files) with standart gorilla\mux functional??

jim....@openscg.com

unread,
Oct 8, 2017, 5:46:33 PM10/8/17
to Gorilla web toolkit
I just came across this poist... any update? I'm a bit disappointed no one else has chimed in. This is definitely something that seems far more difficult than it should be.

BTW, it occurs to me that another approach would be to make MiddleWare a property of a *route*, instead of requiring a new router. In my mind, that means that when Gorilla found a route, it would walk back up the stack looking for routes with MiddleWare, and chaining that MiddleWare together. Not sure which is better...

Matt S

unread,
Oct 9, 2017, 12:03:20 AM10/9/17
to goril...@googlegroups.com

--
You received this message because you are subscribed to the Google Groups "Gorilla web toolkit" group.
To unsubscribe from this group and stop receiving emails from it, send an email to gorilla-web...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

jim....@openscg.com

unread,
Oct 9, 2017, 10:32:55 AM10/9/17
to Gorilla web toolkit
Ahh, thanks for that.

I don't want to pollute the GH issue with questions, so I'll ask here... do you know if that approach will work with subrouters?

Matt S

unread,
Oct 9, 2017, 12:14:53 PM10/9/17
to goril...@googlegroups.com
Feel free to ask in the GitHub Issue, but yes, it will mean that middleware can be scoped to a Subrouter (in the currently proposed design)
Reply all
Reply to author
Forward
0 new messages