Skip to content

Commit 05dd59d

Browse files
committed
Ensure the request store is active during a streamed response.
Adds Rack::BodyProxy in order to hook into the `close` callback that is fired when the request terminates after generating all chunks of the streamed response. Without this patch, the middleware cleans up the request prematurely, after the controller returns, but before all view rendering has completed.
1 parent 1103250 commit 05dd59d

File tree

4 files changed

+49
-8
lines changed

4 files changed

+49
-8
lines changed

lib/request_store/middleware.rb

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
require 'rack/body_proxy'
2+
3+
# A middleware that ensures the RequestStore stays around until
4+
# the last part of the body is rendered. This is useful when
5+
# using streaming.
6+
#
7+
# Uses Rack::BodyProxy, adapted from Rack::Lock's usage of the
8+
# same pattern.
9+
110
module RequestStore
211
class Middleware
312
def initialize(app)
@@ -6,10 +15,18 @@ def initialize(app)
615

716
def call(env)
817
RequestStore.begin!
9-
@app.call(env)
18+
19+
response = @app.call(env)
20+
21+
returned = response << Rack::BodyProxy.new(response.pop) do
22+
RequestStore.end!
23+
RequestStore.clear!
24+
end
1025
ensure
11-
RequestStore.end!
12-
RequestStore.clear!
26+
unless returned
27+
RequestStore.end!
28+
RequestStore.clear!
29+
end
1330
end
1431
end
1532
end

request_store.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Gem::Specification.new do |gem|
1818
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
1919
gem.require_paths = ["lib"]
2020

21+
gem.add_dependency "rack", ">= 1.4"
22+
2123
gem.add_development_dependency "rake", "~> 10.5"
2224
gem.add_development_dependency "minitest", "~> 5.0"
2325
end

test/middleware_test.rb

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,37 +9,57 @@ def setup
99
@middleware = RequestStore::Middleware.new(@app)
1010
end
1111

12+
def call_middleware(opts = {})
13+
_, _, proxy = @middleware.call(opts)
14+
proxy.close
15+
end
16+
1217
def test_middleware_resets_store
13-
2.times { @middleware.call({}) }
18+
2.times do
19+
call_middleware
20+
end
1421

1522
assert_equal 1, @app.last_value
1623
assert_equal({}, RequestStore.store)
1724
end
1825

1926
def test_middleware_resets_store_on_error
2027
e = assert_raises RuntimeError do
21-
@middleware.call({:error => true})
28+
call_middleware error: true
2229
end
2330

2431
assert_equal 'FAIL', e.message
2532
assert_equal({}, RequestStore.store)
2633
end
2734

2835
def test_middleware_begins_store
29-
@middleware.call({})
36+
call_middleware
3037
assert_equal true, @app.store_active
3138
end
3239

3340
def test_middleware_ends_store
34-
@middleware.call({})
41+
call_middleware
42+
3543
assert_equal false, RequestStore.active?
3644
end
3745

3846
def test_middleware_ends_store_on_error
3947
assert_raises RuntimeError do
40-
@middleware.call({:error => true})
48+
call_middleware error: true
4149
end
4250

4351
assert_equal false, RequestStore.active?
4452
end
53+
54+
def test_middleware_stores_until_proxy_closes
55+
_, _, proxy = @middleware.call({})
56+
57+
assert_equal 1, @app.last_value
58+
assert RequestStore.active?
59+
60+
proxy.close
61+
62+
refute RequestStore.active?
63+
refute RequestStore.store[:foo]
64+
end
4565
end

test/test_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ def call(env)
77
@last_value = RequestStore.store[:foo]
88
@store_active = RequestStore.active?
99
raise 'FAIL' if env[:error]
10+
11+
[200, {}, ["response"]]
1012
end
1113
end

0 commit comments

Comments
 (0)