22

I'm trying to rescue from ActionController::RoutingError and I can't get it to work. I tried almost everything that I could find online, including rescue_from ActionController::RoutingError in Rails 4. I have an errors controller and error pages. I got to work cancan access denied and RecordNotFound, but I can solve the RoutingError.

For cancan I use this inside application_controller.rb

rescue_from CanCan::AccessDenied do
    render template: 'errors/error_403', status: 403
  end

and I have this in my routes:

match "/404", to: "errors#error_404", via: :all

If I do the same thing for RoutingError it won't work.

I've also tried match '*path', :to => "errors#error_404" but I get erors.

How can I solve this?

Edit: If I do the same thing for RoutingError as for access denied:

    rescue_from ActionController::RoutingError do
       render template: 'errors/error_404', status: 404
    end

it won't work.

Community
  • 1
  • 1
Bogdan Daniel
  • 2,689
  • 11
  • 43
  • 76

2 Answers2

47

The ActionController::RoutingError is raised when Rails tries to match the request with a route. This happens before Rails even initializes a controller - thus your ApplicationController never has a chance to rescue the exception.

Instead the Rails default exceptions_app kicks in - note that this is an app in the Rack sense - it takes a ENV hash with a request and returns a response - in this case the static /public/404.html file.

What you can do is have your Rails app handle rendering the error pages dynamically instead:

# config/application.rb
config.exceptions_app = self.routes # a Rack Application

# config/routes.rb
match "/404", :to => "errors#not_found", :via => :all
match "/500", :to => "errors#internal_server_error", :via => :all

You would then setup a specific controller to handle the error pages - don't do this in your ApplicationController class as you would be adding a not_found and internal_server_error method to all your controllers!

class ErrorsController < ActionController::Base
  protect_from_forgery with: :null_session

  def not_found
    render(status: 404)
  end

  def internal_server_error
    render(status: 500)
  end
end

Code borrowed from Matt Brictson: Dynamic Rails Error Pages - read it for the full rundown.

max
  • 96,212
  • 14
  • 104
  • 165
  • Thank you for such an elaborate answer. I understand it better now. – Bogdan Daniel May 12 '16 at 23:46
  • Thank you for your explanation that `ActionController::RoutingError` will raise before Rails actually calls the application. I try to follow your suggestion above but I still got `ActionController::RoutingError (No route matches [GET] "/xxxx")` when I try to go to like `localhost:3000/xxxx`. May be I miss something ? – worrawut Jun 12 '18 at 08:59
  • 7
    My bad. I need to tell Rails to render the error page by add this line `config.consider_all_requests_local = false` in `config/environment/development.rb`. Now everything is working fine. – worrawut Jun 12 '18 at 09:07
  • 1
    this solution is not working in production mode of rails with nginx and passenger, in ruby 2.3.8 – vidur punj Aug 02 '19 at 08:39
20

There is a better way to do it:

routes.rb

Rails.application.routes.draw do
  match '*unmatched', to: 'application#route_not_found', via: :all
end

application_controller.rb

class ApplicationController < ActionController::Base
  def route_not_found
    render file: Rails.public_path.join('404.html'), status: :not_found, layout: false
  end
end

To test locally, set the following and restart server.

config/development.rb

config.consider_all_requests_local = false

Tested with Rails 6.

B Seven
  • 44,484
  • 66
  • 240
  • 385