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
self.rescue_handlers = []
end
module ClassMethods
# Rescue exceptions raised in controller actions.
#
# rescue_from receives a series of exception classes or class
# names, and a trailing :with 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 exception.is_a?(klass) 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
#
# protected
# 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)
if handler = handler_for_rescue(exception, object: object)
handler.call exception
exception
elsif exception
rescue_with_handler(exception.cause, object: object)
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