aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/rescuable.rb
blob: 06f6a71de4edce6d63d10307f02f59f8626bb7cb (plain) (blame)
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
require_relative "concern"
require_relative "core_ext/class/attribute"
require_relative "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