Merge pull request #3717 from lest/show-exceptions-refactor

Show exceptions refactor: controller should be responsible for choice to show exceptions
This commit is contained in:
José Valim 2011-11-22 07:19:39 -08:00
commit 39ecbfdab9
7 changed files with 135 additions and 76 deletions

@ -1,5 +1,9 @@
## Rails 3.2.0 (unreleased) ##
* Refactor ActionDispatch::ShowExceptions. Controller is responsible for choice to show exceptions. *Sergey Nartimov*
It's possible to override +show_detailed_exceptions?+ in controllers to specify which requests should provide debugging information on errors.
* Responders now return 204 No Content for API requests without a response body (as in the new scaffold) *José Valim*
* Added ActionDispatch::RequestId middleware that'll make a unique X-Request-Id header available to the response and enables the ActionDispatch::Request#uuid method. This makes it easy to trace requests from end-to-end in the stack and to identify individual requests in mixed logs like Syslog *DHH*

@ -3,6 +3,11 @@ module Rescue
extend ActiveSupport::Concern
include ActiveSupport::Rescuable
included do
config_accessor :consider_all_requests_local
self.consider_all_requests_local = false if consider_all_requests_local.nil?
end
def rescue_with_handler(exception)
if (exception.respond_to?(:original_exception) &&
(orig_exception = exception.original_exception) &&
@ -12,10 +17,15 @@ def rescue_with_handler(exception)
super(exception)
end
def show_detailed_exceptions?
consider_all_requests_local || request.local?
end
private
def process_action(*args)
super
rescue Exception => exception
request.env['action_dispatch.show_detailed_exceptions'] = show_detailed_exceptions?
rescue_with_handler(exception) || raise(exception)
end
end

@ -21,6 +21,8 @@ class Railtie < Rails::Railtie
paths = app.config.paths
options = app.config.action_controller
options.consider_all_requests_local ||= app.config.consider_all_requests_local
options.assets_dir ||= paths["public"].first
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first

@ -2,6 +2,7 @@
require 'action_controller/metal/exceptions'
require 'active_support/notifications'
require 'action_dispatch/http/request'
require 'active_support/deprecation'
module ActionDispatch
# This middleware rescues any exception returned by the application and renders
@ -38,9 +39,9 @@ class ShowExceptions
"application's log file and/or the web server's log file to find out what " <<
"went wrong.</body></html>"]]
def initialize(app, consider_all_requests_local = false)
def initialize(app, consider_all_requests_local = nil)
ActiveSupport::Deprecation.warn "Passing consider_all_requests_local option to ActionDispatch::ShowExceptions middleware no longer works" unless consider_all_requests_local.nil?
@app = app
@consider_all_requests_local = consider_all_requests_local
end
def call(env)
@ -65,11 +66,10 @@ def render_exception(env, exception)
log_error(exception)
exception = original_exception(exception)
request = Request.new(env)
if @consider_all_requests_local || request.local?
rescue_action_locally(request, exception)
if env['action_dispatch.show_detailed_exceptions'] == true
rescue_action_diagnostics(env, exception)
else
rescue_action_in_public(exception)
rescue_action_error_page(exception)
end
rescue Exception => failsafe_error
$stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}"
@ -78,9 +78,9 @@ def render_exception(env, exception)
# Render detailed diagnostics for unhandled exceptions rescued from
# a controller action.
def rescue_action_locally(request, exception)
def rescue_action_diagnostics(env, exception)
template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
:request => request,
:request => Request.new(env),
:exception => exception,
:application_trace => application_trace(exception),
:framework_trace => framework_trace(exception),
@ -98,7 +98,7 @@ def rescue_action_locally(request, exception)
# 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)
def rescue_action_error_page(exception)
status = status_code(exception)
locale_path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
path = "#{public_path}/#{status}.html"

@ -0,0 +1,59 @@
require 'abstract_unit'
module ShowExceptions
class ShowExceptionsController < ActionController::Base
use ActionDispatch::ShowExceptions
def boom
raise 'boom!'
end
end
class ShowExceptionsTest < ActionDispatch::IntegrationTest
test 'show error page from a remote ip' do
@app = ShowExceptionsController.action(:boom)
self.remote_addr = '208.77.188.166'
get '/'
assert_equal "500 error fixture\n", body
end
test 'show diagnostics from a local ip' do
@app = ShowExceptionsController.action(:boom)
['127.0.0.1', '127.0.0.127', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address|
self.remote_addr = ip_address
get '/'
assert_match /boom/, body
end
end
test 'show diagnostics from a remote ip when consider_all_requests_local is true' do
ShowExceptionsController.any_instance.stubs(:consider_all_requests_local).returns(true)
@app = ShowExceptionsController.action(:boom)
self.remote_addr = '208.77.188.166'
get '/'
assert_match /boom/, body
end
end
class ShowExceptionsOverridenController < ShowExceptionsController
private
def show_detailed_exceptions?
params['detailed'] == '1'
end
end
class ShowExceptionsOverridenTest < ActionDispatch::IntegrationTest
test 'show error page' do
@app = ShowExceptionsOverridenController.action(:boom)
get '/', {'detailed' => '0'}
assert_equal "500 error fixture\n", body
end
test 'show diagnostics message' do
@app = ShowExceptionsOverridenController.action(:boom)
get '/', {'detailed' => '1'}
assert_match /boom/, body
end
end
end

@ -2,32 +2,38 @@
class ShowExceptionsTest < ActionDispatch::IntegrationTest
Boomer = lambda do |env|
req = ActionDispatch::Request.new(env)
case req.path
when "/not_found"
raise ActionController::UnknownAction
when "/runtime_error"
raise RuntimeError
when "/method_not_allowed"
raise ActionController::MethodNotAllowed
when "/not_implemented"
raise ActionController::NotImplemented
when "/unprocessable_entity"
raise ActionController::InvalidAuthenticityToken
when "/not_found_original_exception"
raise ActionView::Template::Error.new('template', {}, AbstractController::ActionNotFound.new)
else
raise "puke!"
class Boomer
def initialize(detailed = false)
@detailed = detailed
end
def call(env)
env['action_dispatch.show_detailed_exceptions'] = @detailed
req = ActionDispatch::Request.new(env)
case req.path
when "/not_found"
raise ActionController::UnknownAction
when "/runtime_error"
raise RuntimeError
when "/method_not_allowed"
raise ActionController::MethodNotAllowed
when "/not_implemented"
raise ActionController::NotImplemented
when "/unprocessable_entity"
raise ActionController::InvalidAuthenticityToken
when "/not_found_original_exception"
raise ActionView::Template::Error.new('template', {}, AbstractController::ActionNotFound.new)
else
raise "puke!"
end
end
end
ProductionApp = ActionDispatch::ShowExceptions.new(Boomer, false)
DevelopmentApp = ActionDispatch::ShowExceptions.new(Boomer, true)
ProductionApp = ActionDispatch::ShowExceptions.new(Boomer.new(false))
DevelopmentApp = ActionDispatch::ShowExceptions.new(Boomer.new(true))
test "rescue in public from a remote ip" do
test "rescue with error page when show_exceptions is false" do
@app = ProductionApp
self.remote_addr = '208.77.188.166'
get "/", {}, {'action_dispatch.show_exceptions' => true}
assert_response 500
@ -42,48 +48,8 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
assert_equal "", body
end
test "rescue locally from a local request" do
@app = ProductionApp
['127.0.0.1', '127.0.0.127', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address|
self.remote_addr = ip_address
get "/", {}, {'action_dispatch.show_exceptions' => true}
assert_response 500
assert_match(/puke/, body)
get "/not_found", {}, {'action_dispatch.show_exceptions' => true}
assert_response 404
assert_match(/#{ActionController::UnknownAction.name}/, body)
get "/method_not_allowed", {}, {'action_dispatch.show_exceptions' => true}
assert_response 405
assert_match(/ActionController::MethodNotAllowed/, body)
end
end
test "localize public rescue message" do
# Change locale
old_locale, I18n.locale = I18n.locale, :da
begin
@app = ProductionApp
self.remote_addr = '208.77.188.166'
get "/", {}, {'action_dispatch.show_exceptions' => true}
assert_response 500
assert_equal "500 localized error fixture\n", body
get "/not_found", {}, {'action_dispatch.show_exceptions' => true}
assert_response 404
assert_equal "404 error fixture\n", body
ensure
I18n.locale = old_locale
end
end
test "always rescue locally in development mode" do
test "rescue with diagnostics message when show_exceptions is true" do
@app = DevelopmentApp
self.remote_addr = '208.77.188.166'
get "/", {}, {'action_dispatch.show_exceptions' => true}
assert_response 500
@ -98,6 +64,25 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
assert_match(/ActionController::MethodNotAllowed/, body)
end
test "localize rescue error page" do
# Change locale
old_locale, I18n.locale = I18n.locale, :da
begin
@app = ProductionApp
get "/", {}, {'action_dispatch.show_exceptions' => true}
assert_response 500
assert_equal "500 localized error fixture\n", body
get "/not_found", {}, {'action_dispatch.show_exceptions' => true}
assert_response 404
assert_equal "404 error fixture\n", body
ensure
I18n.locale = old_locale
end
end
test "does not show filtered parameters" do
@app = DevelopmentApp
@ -107,16 +92,15 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
assert_match("&quot;foo&quot;=&gt;&quot;[FILTERED]&quot;", body)
end
test "show registered original exception for wrapped exceptions when consider_all_requests_local is false" do
test "show registered original exception for wrapped exceptions when show_exceptions is false" do
@app = ProductionApp
self.remote_addr = '208.77.188.166'
get "/not_found_original_exception", {}, {'action_dispatch.show_exceptions' => true}
assert_response 404
assert_match(/404 error/, body)
end
test "show registered original exception for wrapped exceptions when consider_all_requests_local is true" do
test "show registered original exception for wrapped exceptions when show_exceptions is true" do
@app = DevelopmentApp
get "/not_found_original_exception", {}, {'action_dispatch.show_exceptions' => true}
@ -125,7 +109,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
end
test "show the controller name in the diagnostics template when controller name is present" do
@app = ProductionApp
@app = DevelopmentApp
get("/runtime_error", {}, {
'action_dispatch.show_exceptions' => true,
'action_dispatch.request.parameters' => {

@ -166,7 +166,7 @@ def default_middleware_stack
middleware.use ::Rack::MethodOverride
middleware.use ::ActionDispatch::RequestId
middleware.use ::Rails::Rack::Logger, config.log_tags # must come after Rack::MethodOverride to properly log overridden methods
middleware.use ::ActionDispatch::ShowExceptions, config.consider_all_requests_local
middleware.use ::ActionDispatch::ShowExceptions
middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
if config.action_dispatch.x_sendfile_header.present?
middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header