The Problem with Rails' Catch-all Route
Today I am upgrading several hundred lines of Rails routes from the old pre 3.0 syntax to the new Rails 3.0 routing DSL. I do like the new syntax, though it is far from a trivial job as there are a number of subtle edge cases I’ve been dealing with. As I meticulously considered and tested each route I discovered some route matches that I didn’t think should have been covered.
For example:
namespace :services do resources :fanships do collection { put :create } end end
This was matching:
GET /services/fanships/create
Which was clearly not what was intended.
Okay, I admit this route is an unusual construction in a Rails app. At first glance I thought maybe something funky was going on due to the conflict of the default resourceful create
route and the additional one I was creating. So I stripped it down:
namespace :services do put 'fanships/create' => 'fanships#create' end
However, even after verifying that there were no other fanships routes getting in the way (the file is over 500 lines) still a match on GET
.
At this point it hit me, the catch-all route:
map.connect ':controller/:action/:id'
This app begin life in Rails 1.1, so the default route has always been there even though the vast majority of the app is served by standard Railsy resources. I never thought much of it, and in fact I’ve relied on it a handful of times over the years. But suddenly doing this exercise it hit me like a ton of bricks that the default route is clobbering all my carefully considered RESTful constraints on URLs. Then I realized that the :controller
symbol is traversing slashes as well, thus even my namespaced controllers aren’t safe. As far as I can tell there is no straightforward way to prevent this effect as long as the catch-all is active.
I’m sure this is no revelation to a lot of peoplein fact there’s a comment about it in the default routes.rb commentsbut I had never thought through the implications. I guess the reason it’s not a bigger problem is because the default resourceful route for show
shields the update
, create
, and destroy
methods from a casual GET
. But even so, this violates the principle of least surprise in a dangerous way. From now on I’ll be sure to never use the default catch-all route.
Jim Kingdon says…
October 28, 2011 at 2:29AM
Well said. Our experience with the catch-all route was similar: it was getting invoked in all kinds of places that we thought we were using the RESTful routes (in addition the older code which we knew was still using the catch-all). Our first step in getting rid of it was to replace it with a bunch of routes like
map.connect 'fanships/:action/:id'
. That, of course, is just for the older code; new code uses RESTful routes.Gabe da Silveira says…
October 28, 2011 at 3:27AM
Thanks for the comment Jim.
I’m actually finding a bit of pollution with RESTful routes in our
routing.rb
as well, and replacing a few of them with named routes like:in cases where we only have one or two actions and little chance of expansion. The benefit of this is that there is an intentionality to each route. When you create a full resource for something that actually only needs one route there’s a bit of intentionality lost.
We’re still 90%
resources
though.Jim Kingdon says…
October 28, 2011 at 7:39PM
We use
:only => [:index]
a lot for cases where we aren’t using all of the RESTful routes.Gabe da Silveira says…
October 29, 2011 at 2:36AM
Yeah that’s a good call.