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
# status_code 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 public/500.da.html
# then attempt to render public/500.html. 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