mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
e35b98e6f5
Follows the same pattern as controllers and jobs. Exceptions raised in delivery jobs (enqueued by `#deliver_later`) are also delegated to the mailer's rescue_from handlers, so you can handle the DeserializationError raised by delivery jobs: ```ruby class MyMailer < ApplicationMailer rescue_from ActiveJob::DeserializationError do … end ``` ActiveSupport::Rescuable polish: * Add the `rescue_with_handler` class method so exceptions may be handled at the class level without requiring an instance. * Rationalize `exception.cause` handling. If no handler matches the exception, fall back to the handler that matches its cause. * Handle exceptions raised elsewhere. Pass `object: …` to execute the `rescue_from` handler (e.g. a method call or a block to instance_exec) against a different object. Defaults to `self`.
165 lines
5.5 KiB
Ruby
165 lines
5.5 KiB
Ruby
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.
|
|
#
|
|
# <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
|
|
#
|
|
# 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
|
|
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 || find_rescue_handler(exception.cause)
|
|
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
|