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 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.