Rails: 捕获 Rails 控制器中的所有异常

有没有一种方法可以捕获 Raills 控制器中所有未捕获的异常,如下所示:

def delete
schedule_id = params[:scheduleId]
begin
Schedules.delete(schedule_id)
rescue ActiveRecord::RecordNotFound
render :json => "record not found"
rescue ActiveRecord::CatchAll
#Only comes in here if nothing else catches the error
end
render :json => "ok"
end

谢谢你

127805 次浏览

rescue with no arguments will rescue any error.

So, you'll want:

def delete
schedule_id = params[:scheduleId]
begin
Schedules.delete(schedule_id)
rescue ActiveRecord::RecordNotFound
render :json => "record not found"
rescue
#Only comes in here if nothing else catches the error
end
render :json => "ok"
end
begin
# do something dodgy
rescue ActiveRecord::RecordNotFound
# handle not found error
rescue ActiveRecord::ActiveRecordError
# handle other ActiveRecord errors
rescue # StandardError
# handle most other errors
rescue Exception
# handle everything else
raise
end

You can also define a rescue_from method.

class ApplicationController < ActionController::Base
rescue_from ActionController::RoutingError, :with => :error_render_method


def error_render_method
respond_to do |type|
type.xml { render :template => "errors/error_404", :status => 404 }
type.all  { render :nothing => true, :status => 404 }
end
true
end
end

Depending on what your goal is, you may also want to consider NOT handling exceptions on a per-controller basis. Instead, use something like the exception_handler gem to manage responses to exceptions consistently. As a bonus, this approach will also handle exceptions that occur at the middleware layer, like request parsing or database connection errors that your application does not see. The exception_notifier gem might also be of interest.

You can catch exceptions by type:

rescue_from ::ActiveRecord::RecordNotFound, with: :record_not_found
rescue_from ::NameError, with: :error_occurred
rescue_from ::ActionController::RoutingError, with: :error_occurred
# Don't resuce from Exception as it will resuce from everything as mentioned here "http://stackoverflow.com/questions/10048173/why-is-it-bad-style-to-rescue-exception-e-in-ruby" Thanks for @Thibaut Barrère for mention that
# rescue_from ::Exception, with: :error_occurred


protected


def record_not_found(exception)
render json: {error: exception.message}.to_json, status: 404
return
end


def error_occurred(exception)
render json: {error: exception.message}.to_json, status: 500
return
end

Actually, if you really want to catch everything, you just create your own exceptions app, which let's you customize the behavior that is usually handled by the PublicExceptions middleware: https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/public_exceptions.rb

A bunch of the other answers share gems that do this for you, but there's really no reason you can't just look at them and do it yourself.

A caveat: make sure you never raise an exception in your exception handler. Otherwise you get an ugly FAILSAFE_RESPONSE https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L4-L22

BTW, the behavior in the controller comes from rescuable: https://github.com/rails/rails/blob/4-2-stable/activesupport/lib/active_support/rescuable.rb#L32-L51

Error handling for a nicer user experience is a very tough thing to pull off correctly.

Here I have provided a fully-complete template to make your life easier. This is better than a gem because its fully customizable to your application.

Note: You can view the latest version of this template at any time on my website: https://westonganger.com/posts/how-to-properly-implement-error-exception-handling-for-your-rails-controllers

Controller

class ApplicationController < ActiveRecord::Base


def is_admin_path?
request.path.split("/").reject{|x| x.blank?}.first == 'admin'
end


private
  

def send_error_report(exception, sanitized_status_number)
val = true


# if sanitized_status_number == 404
#   val = false
# end


# if exception.class == ActionController::InvalidAuthenticityToken
#   val = false
# end


return val
end


def get_exception_status_number(exception)
status_number = 500


error_classes_404 = [
ActiveRecord::RecordNotFound,
ActionController::RoutingError,
]


if error_classes_404.include?(exception.class)
if current_user
status_number = 500
else
status_number = 404
end
end


return status_number.to_i
end


def perform_error_redirect(exception, error_message:)
status_number = get_exception_status_number(exception)


if send_error_report(exception, status_number)
ExceptionNotifier.notify_exception(exception, data: {status: status_number})
end


### Log Error
logger.error exception


exception.backtrace.each do |line|
logger.error line
end


if Rails.env.development?
### To allow for the our development debugging tools
raise exception
end


### Handle XHR Requests
if (request.format.html? && request.xhr?)
render template: "/errors/#{status_number}.html.erb", status: status_number
return
end


if status_number == 404
if request.format.html?
if request.get?
render template: "/errors/#{status_number}.html.erb", status: status_number
return
else
redirect_to "/#{status_number}"
end
else
head status_number
end


return
end


### Determine URL
if request.referrer.present?
url = request.referrer
else
if current_user && is_admin_path? && request.path.gsub("/","") != admin_root_path.gsub("/","")
url = admin_root_path
elsif request.path != "/"
url = "/"
else
if request.format.html?
if request.get?
render template: "/errors/500.html.erb", status: 500
else
redirect_to "/500"
end
else
head 500
end


return
end
end


flash_message = error_message


### Handle Redirect Based on Request Format
if request.format.html?
redirect_to url, alert: flash_message
elsif request.format.js?
flash[:alert] = flash_message
flash.keep(:alert)


render js: "window.location = '#{url}';"
else
head status_number
end
end


rescue_from Exception do |exception|
perform_error_redirect(exception, error_message: I18n.t('errors.system.general'))
end


end

Testing

To test this in your specs you can use the following template:

feature 'Error Handling', type: :controller do


### Create anonymous controller, the anonymous controller will inherit from stated controller
controller(ApplicationController) do
def raise_500
raise Errors::InvalidBehaviour.new("foobar")
end


def raise_possible_404
raise ActiveRecord::RecordNotFound
end
end


before(:all) do
@user = User.first


@error_500 = I18n.t('errors.system.general')
@error_404 = I18n.t('errors.system.not_found')
end


after(:all) do
Rails.application.reload_routes!
end


before :each do
### draw routes required for non-CRUD actions
routes.draw do
get '/anonymous/raise_500'
get '/anonymous/raise_possible_404'
end
end


describe "General Errors" do


context "Request Format: 'html'" do
scenario 'xhr request' do
get :raise_500, format: :html, xhr: true
expect(response).to render_template('errors/500.html.erb')
end


scenario 'with referrer' do
path = "/foobar"


request.env["HTTP_REFERER"] = path


get :raise_500
expect(response).to redirect_to(path)


post :raise_500
expect(response).to redirect_to(path)
end


scenario 'admin sub page' do
sign_in @user


request.path_info = "/admin/foobar"


get :raise_500
expect(response).to redirect_to(admin_root_path)


post :raise_500
expect(response).to redirect_to(admin_root_path)
end


scenario "admin root" do
sign_in @user


request.path_info = "/admin"


get :raise_500
expect(response).to redirect_to("/")


post :raise_500
expect(response).to redirect_to("/")
end


scenario 'public sub-page' do
get :raise_500
expect(response).to redirect_to("/")


post :raise_500
expect(response).to redirect_to("/")
end


scenario 'public root' do
request.path_info = "/"


get :raise_500
expect(response).to render_template('errors/500.html.erb')
expect(response).to have_http_status(500)


post :raise_500
expect(response).to redirect_to("/500")
end


scenario '404 error' do
get :raise_possible_404
expect(response).to render_template('errors/404.html.erb')
expect(response).to have_http_status(404)


post :raise_possible_404
expect(response).to redirect_to('/404')


sign_in @user


get :raise_possible_404
expect(response).to redirect_to('/')


post :raise_possible_404
expect(response).to redirect_to('/')
end
end


context "Request Format: 'js'" do
render_views ### Enable this to actually render views if you need to validate contents
      

scenario 'xhr request' do
get :raise_500, format: :js, xhr: true
expect(response.body).to include("window.location = '/';")


post :raise_500, format: :js, xhr: true
expect(response.body).to include("window.location = '/';")
end


scenario 'with referrer' do
path = "/foobar"


request.env["HTTP_REFERER"] = path


get :raise_500, format: :js
expect(response.body).to include("window.location = '#{path}';")


post :raise_500, format: :js
expect(response.body).to include("window.location = '#{path}';")
end


scenario 'admin sub page' do
sign_in @user


request.path_info = "/admin/foobar"


get :raise_500, format: :js
expect(response.body).to include("window.location = '#{admin_root_path}';")


post :raise_500, format: :js
expect(response.body).to include("window.location = '#{admin_root_path}';")
end


scenario "admin root" do
sign_in @user


request.path_info = "/admin"


get :raise_500, format: :js
expect(response.body).to include("window.location = '/';")


post :raise_500, format: :js
expect(response.body).to include("window.location = '/';")
end


scenario 'public page' do
get :raise_500, format: :js
expect(response.body).to include("window.location = '/';")


post :raise_500, format: :js
expect(response.body).to include("window.location = '/';")
end


scenario 'public root' do
request.path_info = "/"


get :raise_500, format: :js
expect(response).to have_http_status(500)


post :raise_500, format: :js
expect(response).to have_http_status(500)
end


scenario '404 error' do
get :raise_possible_404, format: :js
expect(response).to have_http_status(404)


post :raise_possible_404, format: :js
expect(response).to have_http_status(404)


sign_in @user


get :raise_possible_404, format: :js
expect(response).to have_http_status(200)
expect(response.body).to include("window.location = '/';")


post :raise_possible_404, format: :js
expect(response).to have_http_status(200)
expect(response.body).to include("window.location = '/';")
end
end


context "Other Request Format" do
scenario '500 error' do
get :raise_500, format: :json
expect(response).to have_http_status(500)


post :raise_500, format: :json
expect(response).to have_http_status(500)
end
      

scenario '404 error' do
get :raise_possible_404, format: :json
expect(response).to have_http_status(404)


post :raise_possible_404, format: :json
expect(response).to have_http_status(404)


sign_in @user


get :raise_possible_404, format: :json
expect(response).to have_http_status(500)


post :raise_possible_404, format: :json
expect(response).to have_http_status(500)
end
end


end


end