aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
blob: 71c1e1b9a9c244dce732f0b5dd37ad58157db112 (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
module ActionDispatch
  class ShowExceptions
    include StatusCodes

    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,
      'ActionController::MethodNotAllowed'         => :method_not_allowed,
      'ActionController::NotImplemented'           => :not_implemented,
      'ActionController::InvalidAuthenticityToken' => :unprocessable_entity
    }

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

    RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates')

    cattr_accessor :rescue_responses
    @@rescue_responses = Hash.new(DEFAULT_RESCUE_RESPONSE)
    @@rescue_responses.update DEFAULT_RESCUE_RESPONSES

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

    def initialize(app, consider_all_requests_local = false)
      @app = app
      @consider_all_requests_local = consider_all_requests_local
    end

    def call(env)
      @app.call(env)
    rescue Exception => exception
      raise exception if env['rack.test']

      log_error(exception) if logger

      request = Request.new(env)
      if @consider_all_requests_local || local_request?(request)
        rescue_action_locally(request, exception)
      else
        rescue_action_in_public(exception)
      end
    end

    private
      # Render detailed diagnostics for unhandled exceptions rescued from
      # a controller action.
      def rescue_action_locally(request, exception)
        template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
          :template => template,
          :request => request,
          :exception => exception
        )
        file = "rescues/#{@@rescue_templates[exception.class.name]}.erb"
        body = template.render(:file => file, :layout => 'rescues/layout.erb')

        headers = {'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s}
        status = status_code(exception)

        [status, headers, body]
      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. At first, it will try to render a localized static page.
      # For example, if a 500 error is being handled Rails and locale is :da,
      # it will first attempt to render the file at <tt>public/500.da.html</tt>
      # then attempt to render <tt>public/500.html</tt>. If none of them exist,
      # the body of the response will be left empty.
      def rescue_action_in_public(exception)
        status = status_code(exception)
        locale_path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
        path = "#{public_path}/#{status}.html"

        if locale_path && File.exist?(locale_path)
          render_public_file(status, locale_path)
        elsif File.exist?(path)
          render_public_file(status, path)
        else
          [status, {'Content-Type' => 'text/html', 'Content-Length' => '0'}, []]
        end
      end

      # True if the request came from localhost, 127.0.0.1.
      def local_request?(request)
        request.remote_addr == LOCALHOST && request.remote_ip == LOCALHOST
      end

      def render_public_file(status, path)
        body = File.read(path)
        [status, {'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s}, body]
      end

      def status_code(exception)
        interpret_status(@@rescue_responses[exception.class.name]).to_i
      end

      def public_path
        if defined?(Rails)
          Rails.public_path
        else
          "public"
        end
      end

      def log_error(exception) #:doc:
        ActiveSupport::Deprecation.silence do
          if ActionView::TemplateError === exception
            logger.fatal(exception.to_s)
          else
            logger.fatal(
              "\n#{exception.class} (#{exception.message}):\n  " +
              clean_backtrace(exception).join("\n  ") + "\n\n"
            )
          end
        end
      end

      def clean_backtrace(exception)
        defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ?
          Rails.backtrace_cleaner.clean(exception.backtrace) :
          exception.backtrace
      end

      def logger
        if defined?(Rails.logger)
          Rails.logger
        end
      end
  end
end