Поймать все исключения в контроллере рельсов

90

Есть ли способ перехватить все неуловленные исключения в контроллере рельсов, например:

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

Спасибо

Neigaard
источник

Ответы:

93
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
Крис Джонсен
источник
39
Разве это не правило НИКОГДА не ловить исключения?
RonLugge
2
но как я могу поймать все типы rescue => eтолько в блоке?
Matrix
7
@RonLugge, это полностью зависит от ситуации. применять «никогда» как практическое правило - плохая идея.
Джастин Скилс
11
@JustinSkiles Catching Exception будет обнаруживать синтаксические ошибки (а также сигналы прерывания). Дайте мне один хороший сценарий для этого в производственном коде. Я могу получать сигналы напрямую, но вам нужно сделать это явно, чтобы было понятно, что вы создаете обработчик сигнала. Просто поймать Exception ... плохая, плохая идея. Ловит даже то, что не стоит ловить.
RonLugge
6
Один из немногих распространенных случаев, когда разумно спастись от исключения, - это для целей ведения журнала / отчетности, и в этом случае вы должны немедленно повторно вызвать исключение: stackoverflow.com/a/10048406/252346
aelesbao
199

Вы также можете определить метод 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 драгоценного камня , чтобы управлять ответы на исключения последовательно. В качестве бонуса этот подход также будет обрабатывать исключения, возникающие на уровне промежуточного программного обеспечения, такие как синтаксический анализ запросов или ошибки подключения к базе данных, которые ваше приложение не видит. Также может быть интересен гем exception_notifier .

BOFH
источник
4
Это даже удобнее, поскольку позволяет перехватывать исключения СУХОЙ манерой.
m33lky
А если я использую rescue_from без параметров? будет ли это вести себя так же, как спасение? отловить все ошибки?
minohimself
2
Разве это не плохая практика rescue_from Exception? Насколько я понимаю, лучше от спасения StandardError, чтоб вроде SyntaxErrorи LoadErrorне ловились.
лобати
Да, спасать «Исключение» - дурной тон. См. «Исключительный рубин» Авди Гримма, чтобы узнать, почему это может быть проблематично.
Midwire
35

Вы можете поймать исключения по типу:

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
Мохамед-Ибрахим
источник
2
Будьте осторожны, чтобы не спасать Exceptionнапрямую; см. stackoverflow.com/questions/10048173/…
Тибо Баррер,
10

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
ДрагоценныеТелаЖидкости
источник
8
Старый вопрос, но это неверный ответ. Спасение без аргументов обрабатывает только StandardError robots.oughttbot.com/rescue-standarderror-not-exception
Кейт Гэддис,
0

На самом деле, если вы действительно хотите все уловить , вы просто создаете свое собственное приложение для исключений, которое позволяет вам настраивать поведение, которое обычно обрабатывается промежуточным программным обеспечением PublicExceptions: https://github.com/rails/rails/blob/4-2 -стабильный / Actionpack / lib / action_dispatch / промежуточное ПО / public_exceptions.rb

В куче других ответов есть драгоценные камни, которые делают это за вас, но на самом деле нет причин, по которым вы не можете просто посмотреть на них и сделать это самостоятельно.

Предупреждение: убедитесь, что вы никогда не вызываете исключение в своем обработчике исключений. В противном случае вы получите уродливый 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

BF4
источник
0

Обработка ошибок для более приятного взаимодействия с пользователем - очень сложная задача для правильного выполнения.

Здесь я предоставил полный шаблон, чтобы облегчить вам жизнь. Это лучше, чем драгоценный камень, потому что он полностью настраивается для вашего приложения.

Примечание. Вы можете просмотреть последнюю версию этого шаблона в любое время на моем веб-сайте: 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
Уэстон Гэнджер
источник