Darwinweb

The Problem with Rails' Catch-all Route

October 28, 2011     

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 people—in fact there’s a comment about it in the default routes.rb comments—but 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:

get "resources" => "resources#index"

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.