aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_controller/rescue.rb
blob: cd3a1b56500d8106401d6a95bbc2c8af72741c9e (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
173
174
175
176
module ActionController #:nodoc:
  # Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view
  # (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view
  # is already implemented by the Action Controller, but the public view should be tailored to your specific application. 
  # 
  # The default behavior for public exceptions is to render a static html file with the name of the error code thrown.  If no such 
  # file exists, an empty response is sent with the correct status code.
  #
  # You can override what constitutes a local request by overriding the <tt>local_request?</tt> method in your own controller.
  # Custom rescue behavior is achieved by overriding the <tt>rescue_action_in_public</tt> and <tt>rescue_action_locally</tt> methods.
  module Rescue
    LOCALHOST = '127.0.0.1'.freeze

    DEFAULT_RESCUE_RESPONSE = :internal_server_error
    DEFAULT_RESCUE_RESPONSES = {
      'ActionController::RoutingError'    => :not_found,
      'ActionController::UnknownAction'   => :not_found,
      'ActiveRecord::RecordNotFound'      => :not_found,
      'ActiveRecord::StaleObjectError'    => :conflict,
      'ActiveRecord::RecordInvalid'       => :unprocessable_entity,
      'ActiveRecord::RecordNotSaved'      => :unprocessable_entity
    }

    DEFAULT_RESCUE_TEMPLATE = 'diagnostics'
    DEFAULT_RESCUE_TEMPLATES = {
      'ActionController::MissingTemplate' => 'missing_template',
      'ActionController::RoutingError'    => 'routing_error',
      'ActionController::UnknownAction'   => 'unknown_action',
      'ActionView::TemplateError'         => 'template_error'
    }

    def self.included(base) #:nodoc:
      base.cattr_accessor :rescue_responses
      base.rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE)
      base.rescue_responses.update DEFAULT_RESCUE_RESPONSES

      base.cattr_accessor :rescue_templates
      base.rescue_templates = Hash.new(DEFAULT_RESCUE_TEMPLATE)
      base.rescue_templates.update DEFAULT_RESCUE_TEMPLATES

      base.extend(ClassMethods)
      base.class_eval do
        alias_method_chain :perform_action, :rescue
      end
    end

    module ClassMethods #:nodoc:
      def process_with_exception(request, response, exception)
        new.process(request, response, :rescue_action, exception)
      end
    end

    protected
      # Exception handler called when the performance of an action raises an exception.
      def rescue_action(exception)
        log_error(exception) if logger
        erase_results if performed?

        if consider_all_requests_local || local_request?
          rescue_action_locally(exception)
        else
          rescue_action_in_public(exception)
        end
      end

      # Overwrite to implement custom logging of errors. By default logs as fatal.
      def log_error(exception) #:doc:
        ActiveSupport::Deprecation.silence do
          if ActionView::TemplateError === exception
            logger.fatal(exception.to_s)
          else
            logger.fatal(
              "\n\n#{exception.class} (#{exception.message}):\n    " +
              clean_backtrace(exception).join("\n    ") +
              "\n\n"
            )
          end
        end
      end


      # Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>).  By
      # default will call render_optional_error_file.  Override this method to provide more user friendly error messages.s
      def rescue_action_in_public(exception) #:doc:
        render_optional_error_file response_code_for_rescue(exception)
      end
      
      # Attempts to render a static error page based on the <tt>status_code</tt> thrown,
      # or just return headers if no such file exists. For example, if a 500 error is 
      # being handled Rails will first attempt to render the file at <tt>public/500.html</tt>. 
      # If the file doesn't exist, the body of the response will be left empty
      def render_optional_error_file(status_code)
        status = interpret_status(status_code)
        path = "#{RAILS_ROOT}/public/#{status[0,3]}.html"
        if File.exists?(path)
          render :file => path, :status => status
        else
          head status
        end
      end

      # True if the request came from localhost, 127.0.0.1. Override this
      # method if you wish to redefine the meaning of a local request to
      # include remote IP addresses or other criteria.
      def local_request? #:doc:
        request.remote_addr == LOCALHOST and request.remote_ip == LOCALHOST
      end

      # Render detailed diagnostics for unhandled exceptions rescued from
      # a controller action.
      def rescue_action_locally(exception)
        add_variables_to_assigns
        @template.instance_variable_set("@exception", exception)
        @template.instance_variable_set("@rescues_path", File.dirname(rescues_path("stub")))
        @template.send(:assign_variables_from_controller)

        @template.instance_variable_set("@contents", @template.render_file(template_path_for_local_rescue(exception), false))

        response.content_type = Mime::HTML
        render_file(rescues_path("layout"), response_code_for_rescue(exception))
      end

    private
      def perform_action_with_rescue #:nodoc:
        perform_action_without_rescue
      rescue Exception => exception  # errors from action performed
        if defined?(Breakpoint) && params["BP-RETRY"]
          msg = exception.backtrace.first
          if md = /^(.+?):(\d+)(?::in `(.+)')?$/.match(msg) then
            origin_file, origin_line = md[1], md[2].to_i

            set_trace_func(lambda do |type, file, line, method, context, klass|
              if file == origin_file and line == origin_line then
                set_trace_func(nil)
                params["BP-RETRY"] = false

                callstack = caller
                callstack.slice!(0) if callstack.first["rescue.rb"]
                file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures

                message = "Exception at #{file}:#{line}#{" in `#{method}'" if method}." # `� ( for ruby-mode)

                Breakpoint.handle_breakpoint(context, message, file, line)
              end
            end)

            retry
          end
        end

        rescue_action(exception)
      end

      def rescues_path(template_name)
        "#{File.dirname(__FILE__)}/templates/rescues/#{template_name}.erb"
      end

      def template_path_for_local_rescue(exception)
        rescues_path(rescue_templates[exception.class.name])
      end

      def response_code_for_rescue(exception)
        rescue_responses[exception.class.name]
      end

      def clean_backtrace(exception)
        if backtrace = exception.backtrace
          if defined?(RAILS_ROOT)
            backtrace.map { |line| line.sub RAILS_ROOT, '' }
          else
            backtrace
          end
        end
      end
  end
end