Skip to content

Commit 673c15e

Browse files
BlakeWilliamsEric Wong
authored andcommitted
Add rack.after_reply functionality
This adds `rack.after_reply` functionality which allows rack middleware to pass lambdas that will be executed after the client connection has been closed. This was driven by a need to perform actions in a request that shouldn't block the request from completing but also don't make sense as background jobs. There is prior art of this being supported found in a few gems, as well as this functionality existing in other rack based servers (e.g. Puma). [ew: check if `env' is set in ensure statement] Acked-by: Eric Wong <e@80x24.org>
1 parent 2c34711 commit 673c15e

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

lib/unicorn/http_server.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,8 @@ def process_client(client)
629629
end
630630
end
631631

632+
env["rack.after_reply"] = []
633+
632634
status, headers, body = @app.call(env)
633635

634636
begin
@@ -651,6 +653,8 @@ def process_client(client)
651653
end
652654
rescue => e
653655
handle_error(client, e)
656+
ensure
657+
env["rack.after_reply"].each(&:call) if env
654658
end
655659

656660
def nuke_listeners!(readers)

test/unit/test_server.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,24 @@ def call(env)
3434
end
3535
end
3636

37+
class TestRackAfterReply
38+
def initialize
39+
@called = false
40+
end
41+
42+
def call(env)
43+
while env['rack.input'].read(4096)
44+
end
45+
46+
env["rack.after_reply"] << -> { @called = true }
47+
48+
[200, { 'Content-Type' => 'text/plain' }, ["after_reply_called: #{@called}"]]
49+
rescue Unicorn::ClientShutdown, Unicorn::HttpParserError => e
50+
$stderr.syswrite("#{e.class}: #{e.message} #{e.backtrace.empty?}\n")
51+
raise e
52+
end
53+
end
54+
3755
class WebServerTest < Test::Unit::TestCase
3856

3957
def setup
@@ -114,6 +132,32 @@ def test_early_hints
114132
assert_match %r{^HTTP/1.[01] 200\b}, responses
115133
end
116134

135+
def test_after_reply
136+
teardown
137+
138+
redirect_test_io do
139+
@server = HttpServer.new(TestRackAfterReply.new,
140+
:listeners => [ "127.0.0.1:#@port"])
141+
@server.start
142+
end
143+
144+
sock = TCPSocket.new('127.0.0.1', @port)
145+
sock.syswrite("GET / HTTP/1.0\r\n\r\n")
146+
147+
responses = sock.read(4096)
148+
assert_match %r{\AHTTP/1.[01] 200\b}, responses
149+
assert_match %r{^after_reply_called: false}, responses
150+
151+
sock = TCPSocket.new('127.0.0.1', @port)
152+
sock.syswrite("GET / HTTP/1.0\r\n\r\n")
153+
154+
responses = sock.read(4096)
155+
assert_match %r{\AHTTP/1.[01] 200\b}, responses
156+
assert_match %r{^after_reply_called: true}, responses
157+
158+
sock.close
159+
end
160+
117161
def test_broken_app
118162
teardown
119163
app = lambda { |env| raise RuntimeError, "hello" }

0 commit comments

Comments
 (0)