Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby on Rails choosing wrong controller action

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!

like image 571
Mike Avatar asked Mar 16 '15 21:03

Mike


People also ask

What decides which controller receives Ruby?

Routing decides which controller receives which requests. Often, there is more than one route to each controller, and different routes can be served by different actions. Each action's purpose is to collect information to provide it to a view.

What does Before_action do in Rails?

When writing controllers in Ruby on rails, using before_action (used to be called before_filter in earlier versions) is your bread-and-butter for structuring your business logic in a useful way. It's what you want to use to "prepare" the data necessary before the action executes.

How should you use filters in controllers?

Filters are inherited, so if you set a filter on ApplicationController , it will be run on every controller in your application. The method simply stores an error message in the flash and redirects to the login form if the user is not logged in. If a "before" filter renders or redirects, the action will not run.


1 Answers

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.

like image 193
katafrakt Avatar answered Sep 19 '22 12:09

katafrakt