Skip to content

Commit e28f147

Browse files
committed
Make the test environment show rescuable exceptions in responses
Background ---------- During integration tests, it is desirable for the application to respond as closely as possible to the way it would in production. This improves confidence that the application behavior acts as it should. In Rails tests, one major mismatch between the test and production environments is that exceptions raised during an HTTP request (e.g. `ActiveRecord::RecordNotFound`) are re-raised within the test rather than rescued and then converted to a 404 response. Setting `config.action_dispatch.show_exceptions` to `true` will make the test environment act like production, however, when an unexpected internal server error occurs, the test will be left with a opaque 500 response rather than presenting a useful stack trace. This makes debugging more difficult. This leaves the developer with choosing between higher quality integration tests or an improved debugging experience on a failure. I propose that we can achieve both. Solution -------- Change the configuration option `config.action_dispatch.show_exceptions` from a boolean to one of 3 values: `:all`, `:rescuable`, `:none`. The values `:all` and `:none` behaves the same as the previous `true` and `false` respectively. What was previously `true` (now `:all`) continues to be the default for non-test environments. The new `:rescuable` value is the new default for the test environment. It will show exceptions in the response only for rescuable exceptions as defined by `ActionDispatch::ExceptionWrapper.rescue_responses`. In the event of an unexpected internal server error, the exception that caused the error will still be raised within the test so as to provide a useful stack trace and a good debugging experience.
1 parent 1173d2c commit e28f147

File tree

27 files changed

+235
-121
lines changed

27 files changed

+235
-121
lines changed

actionmailbox/test/dummy/config/environments/test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
config.cache_store = :null_store
3030

3131
# Raise exceptions instead of rendering exception templates.
32-
config.action_dispatch.show_exceptions = false
32+
config.action_dispatch.show_exceptions = :rescuable
3333

3434
# Disable request forgery protection in test environment.
3535
config.action_controller.allow_forgery_protection = false

actionpack/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
* Change `action_dispatch.show_exceptions` to one of `:all`, `:rescuable`, or
2+
`:none`. `:all` and `:none` behave the same as the previous `true` and
3+
`false` respectively. The new `:rescuable` option will only show exceptions
4+
that can be rescued (e.g. `ActiveRecord::RecordNotFound`). `:rescuable` is
5+
now the default for the test environment.
6+
7+
*Jon Dufresne*
8+
19
* `config.action_dispatch.cookies_serializer` now accepts `:message_pack` and
210
`:message_pack_allow_marshal` as serializers. These serializers require the
311
[`msgpack` gem](https://rubygems.org/gems/msgpack) (>= 1.7.0).

actionpack/lib/action_dispatch/http/request.rb

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,13 +191,6 @@ def http_auth_salt
191191
get_header "action_dispatch.http_auth_salt"
192192
end
193193

194-
def show_exceptions? # :nodoc:
195-
# We're treating `nil` as "unset", and we want the default setting to be
196-
# `true`. This logic should be extracted to `env_config` and calculated
197-
# once.
198-
!(get_header("action_dispatch.show_exceptions") == false)
199-
end
200-
201194
# Returns a symbol form of the #request_method.
202195
def request_method_symbol
203196
HTTP_METHOD_LOOKUP[request_method]

actionpack/lib/action_dispatch/middleware/debug_exceptions.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def call(env)
4040
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
4141

4242
invoke_interceptors(request, exception, wrapper)
43-
raise exception unless request.show_exceptions?
43+
raise exception unless wrapper.show?(request)
4444
render_exception(request, exception, wrapper)
4545
end
4646

actionpack/lib/action_dispatch/middleware/exception_wrapper.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,29 @@ def self.status_code_for_exception(class_name)
174174
Rack::Utils.status_code(@@rescue_responses[class_name])
175175
end
176176

177+
def show?(request)
178+
# We're treating `nil` as "unset", and we want the default setting to be
179+
# `:all`. This logic should be extracted to `env_config` and calculated
180+
# once.
181+
config = request.get_header("action_dispatch.show_exceptions")
182+
183+
# Include true and false for backwards compatibility.
184+
case config
185+
when :none
186+
false
187+
when :rescuable
188+
rescue_response?
189+
when true
190+
ActionDispatch.deprecator.warn("Setting action_dispatch.show_exceptions to true is deprecated. Set to :all instead.")
191+
true
192+
when false
193+
ActionDispatch.deprecator.warn("Setting action_dispatch.show_exceptions to false is deprecated. Set to :none instead.")
194+
false
195+
else
196+
true
197+
end
198+
end
199+
177200
def rescue_response?
178201
@@rescue_responses.key?(exception.class.name)
179202
end

actionpack/lib/action_dispatch/middleware/show_exceptions.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,18 @@ def call(env)
3030
@app.call(env)
3131
rescue Exception => exception
3232
request = ActionDispatch::Request.new env
33-
if request.show_exceptions?
34-
render_exception(request, exception)
33+
backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
34+
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
35+
if wrapper.show?(request)
36+
render_exception(request, wrapper)
3537
else
3638
raise exception
3739
end
3840
end
3941

4042
private
41-
def render_exception(request, exception)
42-
backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
43-
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
44-
status = wrapper.status_code
43+
def render_exception(request, wrapper)
44+
status = wrapper.status_code
4545
request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
4646
request.set_header "action_dispatch.original_path", request.path_info
4747
request.set_header "action_dispatch.original_request_method", request.raw_request_method

actionpack/lib/action_dispatch/railtie.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Railtie < Rails::Railtie # :nodoc:
99
config.action_dispatch = ActiveSupport::OrderedOptions.new
1010
config.action_dispatch.x_sendfile_header = nil
1111
config.action_dispatch.ip_spoofing_check = true
12-
config.action_dispatch.show_exceptions = true
12+
config.action_dispatch.show_exceptions = :all
1313
config.action_dispatch.tld_length = 1
1414
config.action_dispatch.ignore_accept_header = false
1515
config.action_dispatch.rescue_templates = {}

actionpack/test/controller/new_base/render_action_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def setup
8989

9090
test "rendering with layout => true" do
9191
assert_raise(ArgumentError) do
92-
get "/render_action/basic/hello_world_with_layout", headers: { "action_dispatch.show_exceptions" => false }
92+
get "/render_action/basic/hello_world_with_layout", headers: { "action_dispatch.show_exceptions" => :none }
9393
end
9494
end
9595

@@ -109,7 +109,7 @@ def setup
109109

110110
test "rendering with layout => 'greetings'" do
111111
assert_raise(ActionView::MissingTemplate) do
112-
get "/render_action/basic/hello_world_with_custom_layout", headers: { "action_dispatch.show_exceptions" => false }
112+
get "/render_action/basic/hello_world_with_custom_layout", headers: { "action_dispatch.show_exceptions" => :none }
113113
end
114114
end
115115
end

actionpack/test/controller/new_base/render_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class RenderTest < Rack::TestCase
7979
end
8080

8181
assert_raises(AbstractController::DoubleRenderError) do
82-
get "/render/double_render", headers: { "action_dispatch.show_exceptions" => false }
82+
get "/render/double_render", headers: { "action_dispatch.show_exceptions" => :none }
8383
end
8484
end
8585
end
@@ -89,13 +89,13 @@ class TestOnlyRenderPublicActions < Rack::TestCase
8989
# Only public methods on actual controllers are callable actions
9090
test "raises an exception when a method of Object is called" do
9191
assert_raises(AbstractController::ActionNotFound) do
92-
get "/render/blank_render/clone", headers: { "action_dispatch.show_exceptions" => false }
92+
get "/render/blank_render/clone", headers: { "action_dispatch.show_exceptions" => :none }
9393
end
9494
end
9595

9696
test "raises an exception when a private method is called" do
9797
assert_raises(AbstractController::ActionNotFound) do
98-
get "/render/blank_render/secretz", headers: { "action_dispatch.show_exceptions" => false }
98+
get "/render/blank_render/secretz", headers: { "action_dispatch.show_exceptions" => :none }
9999
end
100100
end
101101
end

actionpack/test/dispatch/content_security_policy_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ def call(env)
331331
env["action_dispatch.content_security_policy"] = POLICY
332332
env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
333333
env["action_dispatch.content_security_policy_report_only"] = false
334-
env["action_dispatch.show_exceptions"] = false
334+
env["action_dispatch.show_exceptions"] = :none
335335

336336
@app.call(env)
337337
end
@@ -481,7 +481,7 @@ def call(env)
481481
env["action_dispatch.content_security_policy"] = POLICY
482482
env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
483483
env["action_dispatch.content_security_policy_report_only"] = false
484-
env["action_dispatch.show_exceptions"] = false
484+
env["action_dispatch.show_exceptions"] = :none
485485

486486
@app.call(env)
487487
end
@@ -597,7 +597,7 @@ def call(env)
597597
env["action_dispatch.content_security_policy"] = nil
598598
env["action_dispatch.content_security_policy_nonce_generator"] = nil
599599
env["action_dispatch.content_security_policy_report_only"] = false
600-
env["action_dispatch.show_exceptions"] = false
600+
env["action_dispatch.show_exceptions"] = :none
601601

602602
@app.call(env)
603603
end
@@ -653,7 +653,7 @@ def call(env)
653653
env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
654654
env["action_dispatch.content_security_policy_report_only"] = false
655655
env["action_dispatch.content_security_policy_nonce_directives"] = %w(script-src)
656-
env["action_dispatch.show_exceptions"] = false
656+
env["action_dispatch.show_exceptions"] = :none
657657

658658
@app.call(env)
659659
end
@@ -725,7 +725,7 @@ def call(env)
725725
env["action_dispatch.content_security_policy"] = POLICY
726726
env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
727727
env["action_dispatch.content_security_policy_report_only"] = false
728-
env["action_dispatch.show_exceptions"] = false
728+
env["action_dispatch.show_exceptions"] = :none
729729

730730
@app.call(env)
731731
end

0 commit comments

Comments
 (0)