Wyłap wszystkie wyjątki w kontrolerze Rails

90

Czy istnieje sposób na złapanie wszystkich nieobjętych wyjątków w kontrolerze szyn, na przykład:

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

Dziękuję Ci

Neigaard
źródło

Odpowiedzi:

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
Chris Johnsen
źródło
39
Czy nie jest zasadą, że NIGDY nie łapie się wyjątku?
RonLugge
2
ale jak mogę złapać wszystkie typy rescue => etylko w bloku?
Matrix
7
@RonLugge to zależy całkowicie od aktualnej sytuacji. stosowanie zasady „nigdy” jest złym pomysłem.
Justin Skiles,
11
@JustinSkiles Catching Exception wychwytuje błędy składniowe (a także sygnały przerwań). Podaj mi jeden dobry scenariusz robienia tego w kodzie produkcyjnym. Łapię sygnały bezpośrednio, które mogę uzyskać, ale musisz to zrobić wyraźnie, aby było jasne, że tworzysz procedurę obsługi sygnału. Po prostu łapię Wyjątek ... zły, zły pomysł. Łapie nawet rzeczy, których nie powinieneś próbować złapać.
RonLugge,
6
Jednym z nielicznych typowych przypadków, w których rozsądnie jest ratować się przed wyjątkami, jest rejestrowanie / raportowanie, w którym to przypadku należy natychmiast ponownie zgłosić wyjątek: stackoverflow.com/a/10048406/252346
aelesbao
199

Możesz także zdefiniować metodę 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

W zależności od tego, jaki jest twój cel, możesz również rozważyć NIE obsługi wyjątków dla każdego kontrolera. Zamiast używać coś jak exception_handler gem zarządzać odpowiedzi na wyjątkami konsekwentnie. Jako bonus, to podejście będzie obsługiwać również wyjątki, które występują w warstwie oprogramowania pośredniego, takie jak analiza żądań lub błędy połączenia z bazą danych, których aplikacja nie widzi. Exception_notifier klejnot może być również interesujące.

BOFH
źródło
4
Jest to jeszcze bardziej przydatne, ponieważ umożliwia wyłapywanie wyjątków w sposób SUCHY.
m33lky
A jeśli użyję rescue_from bez parametrów? czy będzie to zachowywać się tak samo jak ratunek? złapać wszystkie błędy?
minohimself
2
Czy to nie jest zła praktyka rescue_from Exception? Rozumiem, że lepiej jest ratować StandardError, więc takie rzeczy jak SyntaxErrori LoadErrornie są złapane.
lobati
Tak, ratowanie „Wyjątku” to zła forma. Zobacz „Wyjątkowy rubin” Avdiego Grimma, aby poznać powody, dla których może to być problematyczne.
Midwire
35

Możesz łapać wyjątki według typu:

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
mohamed-ibrahim
źródło
2
Uważaj, aby nie ratować Exceptionbezpośrednio; patrz stackoverflow.com/questions/10048173/…
Thibaut Barrère
10

rescue bez argumentów uratuje każdy błąd.

Więc będziesz chciał:

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
PreciousBodilyFluids
źródło
8
Stare pytanie, ale ta odpowiedź jest nieprawidłowa. ratunek bez argumentów obsługuje tylko roboty
Keith Gaddis
0

Właściwie, jeśli naprawdę chcesz złapać wszystko , po prostu utwórz własną aplikację wyjątków, która pozwoli ci dostosować zachowanie, które jest zwykle obsługiwane przez oprogramowanie pośredniczące PublicExceptions: https://github.com/rails/rails/blob/4-2 -stable / actionpack / lib / action_dispatch / middleware / public_exceptions.rb

Kilka innych odpowiedzi ma takie same klejnoty, które robią to za Ciebie, ale tak naprawdę nie ma powodu, dla którego nie możesz po prostu na nie spojrzeć i zrobić tego samodzielnie.

Ostrzeżenie: upewnij się, że nigdy nie zgłosisz wyjątku w programie obsługi wyjątków. W przeciwnym razie otrzymasz brzydką odpowiedź FAILSAFE_RESPONSE https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L4-L22

BTW, zachowanie w kontrolerze pochodzi z ratunku: https://github.com/rails/rails/blob/4-2-stable/activesupport/lib/active_support/rescuable.rb#L32-L51

BF4
źródło
0

Obsługa błędów w celu zapewnienia przyjemniejszego użytkowania jest bardzo trudna do poprawnego wykonania.

Tutaj dostarczyłem w pełni kompletny szablon, aby ułatwić Ci życie. To jest lepsze niż klejnot, ponieważ można go w pełni dostosować do Twojej aplikacji.

Uwaga: najnowszą wersję tego szablonu możesz w każdej chwili wyświetlić na mojej stronie internetowej: https://westonganger.com/posts/how-to-properly-implement-error-exception-handling-for-your-rails-controllers

Kontroler

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

Testowanie

Aby przetestować to w swoich specyfikacjach, możesz użyć następującego szablonu:

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
Weston Ganger
źródło