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