require 'active_support/core_ext/exception'
require 'active_support/notifications'
require 'action_dispatch/http/request'
require 'goalie/exceptions'
module Goalie
# This middleware rescues any exception returned by the application
# and renders nice exception pages.
class CustomErrorPages
cattr_accessor :rescue_responses
@@rescue_responses = Hash.new(:internal_server_error)
@@rescue_responses.update({
'ActionController::RoutingError' => :not_found,
'AbstractController::ActionNotFound' => :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,
'Goalie::Forbidden' => :forbidden,
'Goalie::NotFound' => :not_found
})
FAILSAFE_RESPONSE = [
500,
{'Content-Type' => 'text/html'},
["
500 Internal Server Error
" <<
"If you are the administrator of this website, then please read " <<
"this web application's log file and/or the web server's log " <<
"file to find out what went wrong."]
]
def initialize(app, consider_all_requests_local = false)
@app = app
@consider_all_requests_local = consider_all_requests_local
end
def call(env)
status, headers, body = @app.call(env)
# Only this middleware cares about RoutingError. So, let's just
# raise it here.
# TODO: refactor this middleware to handle the X-Cascade scenario
# without having to raise an exception.
if headers['X-Cascade'] == 'pass'
raise(ActionController::RoutingError,
"No route matches #{env['PATH_INFO'].inspect}")
end
[status, headers, body]
rescue Exception => exception
render_exception(env, exception)
end
private
def render_exception(env, exception)
log_error(exception)
request = ActionDispatch::Request.new(env)
if @consider_all_requests_local || request.local?
rescue_action_locally(request, exception)
else
rescue_action_in_public(request, exception)
end
rescue Exception => failsafe_error
$stderr.puts("Error during failsafe response: #{failsafe_error}\n" <<
"#{failsafe_error.backtrace * "\n "}")
FAILSAFE_RESPONSE
end
# Render detailed diagnostics for unhandled exceptions rescued from
# a controller action.
def rescue_action_locally(request, exception)
# TODO this should probably move to the controller, that is, have
# http error codes map directly to controller actions, then let
# controller handle different exception classes however it wants
rescue_actions = Hash.new('diagnostics')
rescue_actions.update({
'ActionView::MissingTemplate' => 'missing_template',
'ActionController::RoutingError' => 'routing_error',
'AbstractController::ActionNotFound' => 'unknown_action',
'ActionView::Template::Error' => 'template_error'
})
error_params = {
:request => request, :exception => exception,
:application_trace => application_trace(exception),
:framework_trace => framework_trace(exception),
:full_trace => full_trace(exception)
}
request.env['goalie.error_params'] = error_params
action = rescue_actions[exception.class.name]
response = LocalErrorsController.action(action).call(request.env).last
render(status_code(exception), response.body)
end
def rescue_action_in_public(request, exception)
error_params = {
:request => request, :exception => exception,
:application_trace => application_trace(exception),
:framework_trace => framework_trace(exception),
:full_trace => full_trace(exception)
}
request.env['goalie.error_params'] = error_params
action = @@rescue_responses[exception.class.name]
response = PublicErrorsController.action(action).call(request.env).last
render(status_code(exception), response.body)
end
def status_code(exception)
Rack::Utils.status_code(@@rescue_responses[exception.class.name])
end
def render(status, body)
[status,
{'Content-Type' => 'text/html', 'Content-Length' => body.bytesize.to_s},
[body]]
end
def public_path
defined?(Rails.public_path) ? Rails.public_path : 'public_path'
end
def log_error(exception)
return unless logger
ActiveSupport::Deprecation.silence do
message = "\n#{exception.class} (#{exception.message}):\n"
if exception.respond_to?(:annoted_source_code)
message << exception.annoted_source_code
end
message << " " << application_trace(exception).join("\n ")
logger.fatal("#{message}\n\n")
end
end
def application_trace(exception)
clean_backtrace(exception, :silent)
end
def framework_trace(exception)
clean_backtrace(exception, :noise)
end
def full_trace(exception)
clean_backtrace(exception, :all)
end
def clean_backtrace(exception, *args)
defined?(Rails) && Rails.respond_to?(:backtrace_cleaner) ?
Rails.backtrace_cleaner.clean(exception.backtrace, *args) :
exception.backtrace
end
def logger
defined?(Rails.logger) ? Rails.logger : Logger.new($stderr)
end
end
end