Skip to content

Commit c6e3651

Browse files
Add support for rack.response_finished callbacks in ActionDispatch::Executor.
The executor middleware now supports deferring completion callbacks to later in the request lifecycle by utilizing Rack's `rack.response_finished` mechanism, when available. This enables applications to define `rack.response_finished` callbacks that may rely on state that would be cleaned up by the executor's completion callbacks. Co-authored-by: Hartley McGuire <[email protected]>
1 parent afd3591 commit c6e3651

File tree

3 files changed

+57
-4
lines changed

3 files changed

+57
-4
lines changed

actionpack/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
* Add support for `rack.response_finished` callbacks in ActionDispatch::Executor.
2+
3+
The executor middleware now supports deferring completion callbacks to later
4+
in the request lifecycle by utilizing Rack's `rack.response_finished` mechanism,
5+
when available. This enables applications to define `rack.response_finished` callbacks
6+
that may rely on state that would be cleaned up by the executor's completion callbacks.
7+
8+
*Adrianna Chang*, *Hartley McGuire*
9+
110
* Allow hosts redirects from `hosts` Rails configuration
211

312
```ruby

actionpack/lib/action_dispatch/middleware/executor.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ def initialize(app, executor)
1212

1313
def call(env)
1414
state = @executor.run!(reset: true)
15+
if response_finished = env["rack.response_finished"]
16+
response_finished << -> { state.complete! }
17+
end
18+
1519
begin
1620
response = @app.call(env)
1721

@@ -20,15 +24,21 @@ def call(env)
2024
@executor.error_reporter.report(error, handled: false, source: "application.action_dispatch")
2125
end
2226

23-
returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
27+
unless response_finished
28+
response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
29+
end
30+
returned = true
31+
response
2432
rescue Exception => error
2533
request = ActionDispatch::Request.new env
2634
backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
2735
wrapper = ExceptionWrapper.new(backtrace_cleaner, error)
2836
@executor.error_reporter.report(wrapper.unwrapped_exception, handled: false, source: "application.action_dispatch")
2937
raise
3038
ensure
31-
state.complete! unless returned
39+
if !returned && !response_finished
40+
state.complete!
41+
end
3242
end
3343
end
3444
end

actionpack/test/dispatch/executor_test.rb

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,10 +170,44 @@ def test_handled_error_is_not_reported
170170
end
171171
end
172172

173+
def test_complete_callbacks_are_called_on_rack_response_finished
174+
completed = false
175+
executor.to_complete { completed = true }
176+
177+
env = { "rack.response_finished" => [] }
178+
call_and_return_body(env)
179+
180+
assert_not completed
181+
182+
assert_equal 1, env["rack.response_finished"].size
183+
env["rack.response_finished"].first.call
184+
185+
assert completed
186+
end
187+
188+
def test_complete_callbacks_are_called_once_on_rack_response_finished_when_exception_is_raised
189+
completed_count = 0
190+
executor.to_complete { completed_count += 1 }
191+
192+
env = { "rack.response_finished" => [] }
193+
194+
begin
195+
call_and_return_body(env) do
196+
raise "error"
197+
end
198+
rescue
199+
end
200+
201+
assert_equal 1, env["rack.response_finished"].size
202+
env["rack.response_finished"].first.call
203+
204+
assert_equal 1, completed_count
205+
end
206+
173207
private
174-
def call_and_return_body(&block)
208+
def call_and_return_body(env = {}, &block)
175209
app = block || proc { [200, {}, []] }
176-
env = Rack::MockRequest.env_for("", {})
210+
env = Rack::MockRequest.env_for("", env)
177211
_, _, body = middleware(app).call(env)
178212
body
179213
end

0 commit comments

Comments
 (0)