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

Posted

技术标签:

【中文标题】Rails:在 Rails 控制器中捕获所有异常【英文标题】:Rails: Catch all exceptions in a rails controller 【发布时间】:2011-04-11 06:49:48 【问题描述】:

有没有办法在 Rails 控制器中捕获所有未捕获的异常,如下所示:

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

谢谢

【问题讨论】:

【参考方案1】:

你也可以定义一个rescue_from方法。

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

根据您的目标,您可能还需要考虑不在每个控制器的基础上处理异常。相反,使用类似exception_handler gem 的东西来一致地管理对异常的响应。作为奖励,这种方法还将处理中间件层发生的异常,例如您的应用程序看不到的请求解析或数据库连接错误。 exception_notifier gem 可能也很有趣。

【讨论】:

这更加方便,因为它允许以 DRY 方式捕获异常。 如果我使用没有参数的rescue_from?这会和救援一样吗?捕获所有错误? rescue_from Exception 不是坏习惯吗?我的理解是最好从StandardError中解救出来,这样SyntaxErrorLoadError这样的东西就不会被抓到了。 是的,拯救“异常”是不好的形式。请参阅 Avdi Grimm 的“Exceptional Ruby”,了解为什么会出现问题。【参考方案2】:
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

【讨论】:

规则不是从不捕获异常吗? 但我如何才能捕获 rescue =&gt; e 块中的所有类型? @RonLugge 这完全取决于手头的情况。将“从不”作为经验法则是个坏主意。 @JustinSkiles 捕获异常将捕获语法错误(以及中断信号)。给我一个在生产代码中执行此操作的好方案。我可以直接捕获信号,但是您需要明确地这样做以明确您正在创建信号处理程序。只是捕捉异常......坏,坏主意。甚至可以捕捉到你不应该尝试捕捉的东西。 从异常中拯救出来的少数常见情况之一是用于记录/报告目的,在这种情况下,您应该立即重新引发异常:***.com/a/10048406/252346【参考方案3】:

您可以按类型捕获异常:

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://***.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

【讨论】:

注意不要直接从Exception解救;见***.com/questions/10048173/…【参考方案4】:

rescue 不带参数将挽救任何错误。

所以,你会想要:

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

【讨论】:

过时的问题,但这个答案是不正确的。没有参数的救援只处理标准错误robots.thoughtbot.com/rescue-standarderror-not-exception【参考方案5】:

为了更好的用户体验而进行错误处理是一件非常困难的事情。

在这里,我提供了一个完整的模板,让您的生活更轻松。这比 gem 更好,因为它完全可以为您的应用程序定制。

注意:您可以随时在我的网站上查看此模板的最新版本:https://westonganger.com/posts/how-to-properly-implement-error-exception-handling-for-your-rails-controllers

控制器

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

测试

要在您的规范中对此进行测试,您可以使用以下模板:

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

【讨论】:

【参考方案6】:

实际上,如果你真的想捕捉所有东西,你只需创建自己的异常应用程序,它可以让你自定义通常由 PublicExceptions 中间件处理的行为:https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/public_exceptions.rb

堆栈中的位置https://github.com/rails/rails/blob/4-2-stable/railties/lib/rails/application/default_middleware_stack.rb#L98-L99 配置https://github.com/rails/rails/blame/4-2-stable/guides/source/configuring.md#L99 这可以像使用路由 http://blog.plataformatec.com.br/2012/01/my-five-favorite-hidden-features-in-rails-3-2/ 或自定义控制器一样简单(但请参阅 https://github.com/rails/rails/pull/17815 了解不使用路由的原因)

许多其他答案分享了可以为您做到这一点的宝石,但您真的没有理由不能只看它们并自己做。

一个警告:确保您永远不会在您的异常处理程序中引发异常。否则你会得到一个丑陋的 FAILSAFE_RESPONSE https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L4-L22

顺便说一句,控制器中的行为来自可恢复的:https://github.com/rails/rails/blob/4-2-stable/activesupport/lib/active_support/rescuable.rb#L32-L51

【讨论】:

以上是关于Rails:在 Rails 控制器中捕获所有异常的主要内容,如果未能解决你的问题,请参考以下文章

rake 任务中的 Rails 异常通知器

Ruby on Rails - 未捕获的ReferenceError:$未定义

Ruby on Rails - 未捕获的 ReferenceError:$ 未定义

写入 Rails 控制台

阻止用户返回并查看以前提交的表单Rails

Rails 6中的protect_from_forgery?