rescue_from ActionController::RoutingError doesn't work - ruby-on-rails

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.

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.

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.

Related

How to use rescue_from with Rails 6?

Apparently rescue_from is supposed to catch Exceptions, but this does not work as expected:
class ApplicationController < ActionController::Base
rescue_from ActionController::RoutingError, with: :not_found
def not_found
text: 'Not found'
end
end
Spec:
specify 'Not found' do
visit '/zzz'
expect(page.status_code).to eq 200
end
Failure/Error: visit '/zzz'
ActionController::RoutingError:
No route matches [GET] "/zzz"
Same behavior in development environment.
However, rescuing other errors such as RuntimeError does work as expected.
Docs: https://apidock.com/rails/v6.0.0/ActiveSupport/Rescuable/ClassMethods/rescue_from
Rails 6.0.2
Why can't RoutingError be used with rescue_from? Is RoutingError raised in middleware or by the router before the controller is called? Is there another way to catch RoutingError?
Router errors are raised before reaching a controller, you could have a wildcard route to match anything that's not matched by other routes at the end of the routes.rb file an point that route to a specific controller action.
Something like:
match '*foo', to: 'application#not_found'
(Didn't try that, you may need to tweak it a little but I think the idea is clear)

Rails - Error pages controller and redirecting the user?

Building a small app and I'd like to intercept error pages.
Is it possible to define a controller where all of Rails's failed page requests end up so I can add my custom behavior there?
Place this at the bottom of your app config/routes.rb
match '*path', to: redirect('/'), via: :all
Correct me if I'm wrong, but I think rescuing the error from the controller won't work because the request hasn't hit any controller yet for you to do a rescue_from. (Just tried this on 4.2.x)
You will need to add a catch-all kind of route into your config/routes.rb, right at the end of the all the others. This basically routes all the traffic that is not recognized to the errors#show action.
YourApp::Application.routes.draw do
# ...
# ...
get '*path', to: 'errors#show', via: :all
end
Then create the ErrorsController and the method for you to parse the URL.
class ErrorsController < ApplicationController
def show
# parse the URL and redirect or simply render the page
end
end
Do note that the above will not help with ActiveRecord::RecordNotFound errors; because they fall into a route pattern for a specific controller. For that, you can add rescue_from in the ApplicationController as such:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :foo
def foo
# redirect or do something
end
end

Rendering custom error page from admin namespace

I have custom error pages in a Rails 4 application and a method in ApplicationController that I use in certain places to manually raise a RoutingError:
def not_found
raise ActionController::RoutingError.new('Not Found')
end
The ErrorsController has a file_not_found action with a corresponding view. routes.rb has the following content:
match '/404', to: 'errors#file_not_found', via: :all
All of this allows me to write code such as not_found unless item.available in my controllers to force a 404.
I'm currently getting the following error only when I call not_found from the admin namespace: Missing template [project path]/public/404.html. How can I use not_found from admin using the same views?
I ommitted a detail that I didn't think was relevant but turned out to be crucial: I am using rails_admin. While perusing the source for this excellent gem, I noticed that it defines its own not_found method here:
def not_found
render :file => Rails.root.join('public', '404.html'), :layout => false, :status => 404
end
To override it, I added the following code in config/initializers/rails_admin_not_found.rb:
RailsAdmin::ApplicationController.module_eval do
def not_found
raise ActionController::RoutingError.new('Not Found')
end
end

Redirect to log in page if user is not authenticated with Devise

I'm using Devise with Ruby on Rails.
What is the recommended way to redirect unauthenticated users to the sessions#new page if they attempt to access a page that requires authentication?
Right now I get an error that says no route matches the one they attempt to access (leading to a 404 error in production).
Just simple add this method to application_controller.rb
protected
def authenticate_user!
if user_signed_in?
super
else
redirect_to login_path, :notice => 'if you want to add a notice'
## if you want render 404 page
## render :file => File.join(Rails.root, 'public/404'), :formats => [:html], :status => 404, :layout => false
end
end
And you can call this method on before_filter another controllers you want.
e.g :
class HomesController < ApplicationController
before_filter :authenticate_user!
## if you want spesific action for require authentication
## before_filter :authenticate_user!, :only => [:action1, :action2]
end
Don't forget add login_path into routes.rb
devise_scope :user do
match '/sign-in' => "devise/sessions#new", :as => :login
end
note : I always use this way when play with devise for my apps authentication.. (rails 3.2 and rails 4.0.1)
You can do just like GeekTol wrote, or just put
before_action :authenticate_user!
in your controller.
In this case, devise uses the default authenticate_user! method, that will redirect to the "user_session_path" and use the default flash message.
It's not necessary to rewrite authenticate_user! method, unless you want to customize it.
I thought you could just add:
before_action :authenticate_user!
to each controller that required the user to be logged in.
I'm a Rails beginner but I found this in my own searches and it works well in my application.
You should refer to Devise's own How To: How To: Redirect to a specific page when the user can not be authenticated.
Another alternative I can think of is creating a routing Constraint wrapping your protected routes. You'd better stick to Devise's way, but here is an example:
#On your routes.rb
constraints(Constraints::LoginRequired) do
get '/example'
end
#Somewhere like lib/constraints/login_required.rb
module Constraints
class LoginRequired
def self.matches?(request)
#some devise code that checks if the user is logged in
end
end
end
Add this code in your config/routes.rb devise_for :users and resources :users and you can generate devise in views.

Custom 404 not firing from controller

I'm trying to handle 404 errors when using a cms gem. In my past application this 404 method seemed to work but now its not rescuing the error. Any ideas?
# application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
rescue_from ActionController::RoutingError, :with => :render_404
def after_sign_in_path_for(resource)
admin_cms_path
end
private
def render_404(exception = nil)
if exception
logger.info "Rendering 404: #{exception.message}"
end
flash[:error] = 'The page you entered is unavailble, please send us an email if this is unexpected'
redirect_to root_path
end
end
and the routes
# routes.rb
Amskier::Application.routes.draw do
# ...
comfy_route :blog_admin, path: '/admin'
comfy_route :cms_admin, path: '/admin'
root to: "cms/content#show"
comfy_route :cms, path: '/', sitemap: false
end
Add the route to this action of controller:
match 'path' to: 'errors#catch_404'
and create errors controller like this
class ErrorsController < ApplicationController
def catch_404
raise ActionController::RoutingError.new(params[:path])
end
end
after this it should start working.

Resources