1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
|
# frozen_string_literal: true
require "active_support/concern"
require "active_support/core_ext/class/attribute"
require "active_support/core_ext/string/inflections"
module ActiveSupport
# Rescuable module adds support for easier exception handling.
module Rescuable
extend Concern
included do
class_attribute :rescue_handlers, default: []
end
module ClassMethods
# Rescue exceptions raised in controller actions.
#
# <tt>rescue_from</tt> receives a series of exception classes or class
# names, and a trailing <tt>:with</tt> option with the name of a method
# or a Proc object to be called to handle them. Alternatively a block can
# be given.
#
# Handlers that take one argument will be called with the exception, so
# that the exception can be inspected when dealing with it.
#
# Handlers are inherited. They are searched from right to left, from
# bottom to top, and up the hierarchy. The handler of the first class for
# which <tt>exception.is_a?(klass)</tt> holds true is the one invoked, if
# any.
#
# class ApplicationController < ActionController::Base
# rescue_from User::NotAuthorized, with: :deny_access # self defined exception
# rescue_from ActiveRecord::RecordInvalid, with: :show_errors
#
# rescue_from 'MyAppError::Base' do |exception|
# render xml: exception, status: 500
# end
#
# private
# def deny_access
# ...
# end
#
# def show_errors(exception)
# exception.record.new_record? ? ...
# end
# end
#
# Exceptions raised inside exception handlers are not propagated up.
def rescue_from(*klasses, with: nil, &block)
unless with
if block_given?
with = block
else
raise ArgumentError, "Need a handler. Pass the with: keyword argument or provide a block."
end
end
klasses.each do |klass|
key = if klass.is_a?(Module) && klass.respond_to?(:===)
klass.name
elsif klass.is_a?(String)
klass
else
raise ArgumentError, "#{klass.inspect} must be an Exception class or a String referencing an Exception class"
end
# Put the new handler at the end because the list is read in reverse.
self.rescue_handlers += [[key, with]]
end
end
# Matches an exception to a handler based on the exception class.
#
# If no handler matches the exception, check for a handler matching the
# (optional) exception.cause. If no handler matches the exception or its
# cause, this returns +nil+, so you can deal with unhandled exceptions.
# Be sure to re-raise unhandled exceptions if this is what you expect.
#
# begin
# …
# rescue => exception
# rescue_with_handler(exception) || raise
# end
#
# Returns the exception if it was handled and +nil+ if it was not.
def rescue_with_handler(exception, object: self, visited_exceptions: [])
visited_exceptions << exception
if handler = handler_for_rescue(exception, object: object)
handler.call exception
exception
elsif exception
if visited_exceptions.include?(exception.cause)
nil
else
rescue_with_handler(exception.cause, object: object, visited_exceptions: visited_exceptions)
end
end
end
def handler_for_rescue(exception, object: self) #:nodoc:
case rescuer = find_rescue_handler(exception)
when Symbol
method = object.method(rescuer)
if method.arity == 0
-> e { method.call }
else
method
end
when Proc
if rescuer.arity == 0
-> e { object.instance_exec(&rescuer) }
else
-> e { object.instance_exec(e, &rescuer) }
end
end
end
private
def find_rescue_handler(exception)
if exception
# Handlers are in order of declaration but the most recently declared
# is the highest priority match, so we search for matching handlers
# in reverse.
_, handler = rescue_handlers.reverse_each.detect do |class_or_name, _|
if klass = constantize_rescue_handler_class(class_or_name)
klass === exception
end
end
handler
end
end
def constantize_rescue_handler_class(class_or_name)
case class_or_name
when String, Symbol
begin
# Try a lexical lookup first since we support
#
# class Super
# rescue_from 'Error', with: …
# end
#
# class Sub
# class Error < StandardError; end
# end
#
# so an Error raised in Sub will hit the 'Error' handler.
const_get class_or_name
rescue NameError
class_or_name.safe_constantize
end
else
class_or_name
end
end
end
# Delegates to the class method, but uses the instance as the subject for
# rescue_from handlers (method calls, instance_exec blocks).
def rescue_with_handler(exception)
self.class.rescue_with_handler exception, object: self
end
# Internal handler lookup. Delegates to class method. Some libraries call
# this directly, so keeping it around for compatibility.
def handler_for_rescue(exception) #:nodoc:
self.class.handler_for_rescue exception, object: self
end
end
end
|