7

Today I came across some strange (and very inconvenient) Ruby on Rails behavior that even persistent combing of the net did not yield a satisfying answer to. Note: I translated the method and route names to be easier to read in English, and hope I did not introduce any inconsistencies.

Situation

Environment

Ruby on Rails 4.2.0 executing under Ruby 2.0 (also tested under Ruby 2.2.0)

Relevant Code

consider a controller with these actions, among others:

class AssignmentsController < ApplicationController
  def update
    ...
  end

  def takeover_confirmation
    ...
  end
end

routes.rb

Since I use a lot of manually defined routes, I did not use resources in routes.rb. The routes in question are defined as follows:

...
post 'assignments/:id' => 'assignments#update', as: 'assignment'
post 'assignments/takeover_confirmation' => 'assignments#takeover_confirmation'
...

The relevant output of rake routes:

assignment POST  /assignments/:id(.:format)  assignments#update
assignments_takeover_confirmation  POST  /assignments/takeover_confirmation(.:format) assignments#takeover_confirmation

Problem

When I do a POST to the assignments_takeover_confirmation_path, rails routes it to the update method instead. Server log:

Started POST "/assignments/takeover_confirmation" for ::1 at ...
Processing by AssignmentsController#update as HTML

Mitigation

If I put the update route definition after the takeover_confirmation one, it works as intended (didn't check a POST to update though).

Furthermore, after writing all this I found out I used the wrong request type for the update method in routes.rb (POST instead of PATCH). Doing this in routes.rb does indeed solve my problem:

patch 'assignments/:id' => 'assignments#update', as: 'assignment'

However, even when defining it as POST, Rails should not direct a POST request to the existing path "/assignments/takeover_confirmation" to a completely different action, should it? I fear the next time I use two POST routes for the same controller it will do the same thing again.

It seems I have a severe misconception of Rails routing, but cannot lay my finger on it...

Edit: Solution

As katafrakt explained, the above request to /assignments/takeover_confirmation matched the route assignments/:id because Rails interpreted the "takeover_confirmation" part as string and used it for the :id parameter. Thus, this is perfectly expected behavior.

Working Example

For the sake of completeness, here is a working (if minimalistic) route-definition that does as it should, inspired by Chris's comment:

  resources :assignments do
    collection do
      post 'takeover_confirmation'
    end
  end

In this example, only my manually created route is explicitly defined. The routes for update, show, etc. (that I defined manually at first) are now implicitly defined by resources: :assignments.

Corresponding excerpt from rake routes:

...
takeover_confirmation_assignments  POST  /assignments/takeover_confirmation(.:format) assignments#takeover_confirmation
...
assignment GET    /assignments/:id(.:format)  assignments#show
           PATCH  /assignments/:id(.:format)  assignments#update
           PUT    /assignments/:id(.:format)  assignments#update
           DELETE /assignments/:id(.:format)  assignments#destroy
....

Thanks for the help!

Mike
  • 73
  • 1
  • 4
  • maybe I missed something when reading your question, but why aren't you using `resources` in your routes file? ...ohhh I see your using a lot of manually defined routes. Well what happens when you use `resources`? Do you get the desired behavior you are seeking? – ipatch Mar 16 '15 at 21:14
  • trying to refactor that right now... will let you know how it works out. – Mike Mar 16 '15 at 21:29
  • Thanks for the comment. I edited the "Solution" section and placed a working example there based on your suggestion. – Mike Mar 16 '15 at 22:05

1 Answers1

9

However, even when defining it as POST, Rails should not direct a POST request to the existing path "/assignments/takeover_confirmation" to a completely different action, should it?

It should. Rails routing is matched in exact same order as defined in routes.rb file (from top to bottom). So if it matches a certain rule (and /assignments/takeover_confirmation matches assignments/:id rule) it stops processing the routing.

This behaviour is simple and efficient. I imagine that any kind of "smart" matching the best route would result in cumbersome and unexpected results.

BTW that's why catch-all route used to be defined at the very bottom of routing file.

katafrakt
  • 2,438
  • 15
  • 19
  • ARGH! It never even occurred to me that you could interpret the string "takeover_confirmation" as a parameter and pass it as :id. In fact, this is exactly what happened later, when the line `Assignment.find params[:id]` in my update method complained about not finding an object with id "takeover_confirmation". So it was right to match the route like that - I was just not seeing it! – Mike Mar 16 '15 at 21:54
  • You could use constraints to avoid that. Like `post 'assignments/:id' => 'assignments#update', as: 'assignment', constraints: { id: /\d.+/ }`. – katafrakt Mar 16 '15 at 22:29
  • thanks, I did not know about this, and it might come in handy in similar situations. However, I think it is cleaner to avoid such ambiguity in the first place - after all, it was no problem after you explained _why_ it was happening at all. – Mike Mar 24 '15 at 20:58