Monday, December 19, 2011

Rails 3 Routes.rb - Helpers for Legacy Routes?

During our Rails 3 upgrade we ran into a non-obvious routing issue. Pre-upgrade we had a person route defined as follows:

match '/person/email' => 'person/emails#index'
map.resources :person do |person|
  person.resources :emails
end

Unfortunately we have to include the 'match' rule for backwards compatibility for older clients. This worked great until we upgraded to Rails 3. When we ran rake routes we got the following output:

person_emails     GET    /person/emails(.:format)          {:action=>"index", :controller=>"person/emails"}
                  POST   /person/emails(.:format)          {:action=>"create", :controller=>"person/emails"}
new_person_email  GET    /person/emails/new(.:format)      {:action=>"new", :controller=>"person/emails"}
edit_person_email GET    /person/emails/:id/edit(.:format) {:action=>"edit", :controller=>"person/emails"}
person_email      GET    /person/emails/:id(.:format)      {:action=>"show", :controller=>"person/emails"}
                  PUT    /person/emails/:id(.:format)      {:action=>"update", :controller=>"person/emails"}
                  DELETE /person/emails/:id(.:format)      {:action=>"destroy", :controller=>"person/emails"}
                         /person/email(.:format)           {:action=>"index", :controller=>"person/emails"}

Everything looks like it's supposed to. Notice that the last line in the 'rake routes' output is the 'match' rule. From within out app we were using the person_email_path( @email ) helper and would produce the URL /person/emails/34dga3-fg7899-aa645c. Everything was working perfectly until we upgraded to Rails 3. During the upgrade we avoided making drastic changes to routes.rb but we had to adapted it to Rails' new way of defining resources, but other than that our routes stayed the same. Our Rails 3 routes.rb file is below:

match '/person/email' => 'person/emails#index'
namespace :person do
  resources :emails
end

Now unexpectedly, the output of the person_email_path( @email ) helper produced the following /person/emails.34dga3-fg7899-aa645c. Additionally, after running rake routes, we observed the following output:

person_email             /person/email(.:format)           {:action=>"index", :controller=>"person/emails"}
person_emails     GET    /person/emails(.:format)          {:action=>"index", :controller=>"person/emails"}
                  POST   /person/emails(.:format)          {:action=>"create", :controller=>"person/emails"}
new_person_email  GET    /person/emails/new(.:format)      {:action=>"new", :controller=>"person/emails"}
edit_person_email GET    /person/emails/:id/edit(.:format) {:action=>"edit", :controller=>"person/emails"}
                  GET    /person/emails/:id(.:format)      {:action=>"show", :controller=>"person/emails"}
                  PUT    /person/emails/:id(.:format)      {:action=>"update", :controller=>"person/emails"}
                  DELETE /person/emails/:id(.:format)      {:action=>"destroy", :controller=>"person/emails"}

If you compare the two rake routes output closely you'll notice that after upgrading to Rails 3 the
match '/person/email' => 'person/emails#index'
rule hijacked the person_email_path helper. Previously this helper was used to generate the URL to the #show action, now after the upgrade it's being used to match the rule supporting backwards compatibility, which leads clients to the #index action.

Fortunately the fix for this was really simple, the hard put was tracking it down. All we had to do was move the rule supporting backwards compatibility below the resource definitions, unfortunately we have a lot of legacy clients and weren't able to remove rule. Below is the fixed routes.rb:

namespace :person do
  resources :emails
end
match '/person/email' => 'person/emails#index'

I don't know if this is a defect in Rails, or if Rails is just trying to be cute. I was ( and still am ) under the impression that the path and URL helpers were only applicable to resources. In any case, I hope this post helps someone.