Catch all exceptions in a rails controller

后端 未结 6 936
春和景丽
春和景丽 2020-11-28 02:20

Is there a way to catch all uncatched exceptions in a rails controller, like this:

def delete
  schedule_id = params[:scheduleId]
  begin
    Schedules.delet         


        
相关标签:
6条回答
  • 2020-11-28 03:03

    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
    
    0 讨论(0)
  • 2020-11-28 03:07

    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
    
    0 讨论(0)
  • 2020-11-28 03:09

    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.

    0 讨论(0)
  • 2020-11-28 03:13
    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
    
    0 讨论(0)
  • 2020-11-28 03:14

    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

    • location in stack https://github.com/rails/rails/blob/4-2-stable/railties/lib/rails/application/default_middleware_stack.rb#L98-L99
    • configuring https://github.com/rails/rails/blame/4-2-stable/guides/source/configuring.md#L99
      • which can be as easy as using the routes http://blog.plataformatec.com.br/2012/01/my-five-favorite-hidden-features-in-rails-3-2/ or a custom controller (but see https://github.com/rails/rails/pull/17815 for reasons not to use the routes)

    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

    0 讨论(0)
  • 2020-11-28 03:19

    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
    
    0 讨论(0)
提交回复
热议问题