Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/puma/binder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ def initialize(log_writer, conf = Configuration.new, env: ENV)

"QUERY_STRING".freeze => "",
SERVER_SOFTWARE => PUMA_SERVER_STRING,
GATEWAY_INTERFACE => CGI_VER
GATEWAY_INTERFACE => CGI_VER,

RACK_AFTER_REPLY => nil,
RACK_RESPONSE_FINISHED => nil,
}

@envs = {}
Expand Down
1 change: 1 addition & 0 deletions lib/puma/const.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ module Const
RACK_INPUT = "rack.input"
RACK_URL_SCHEME = "rack.url_scheme"
RACK_AFTER_REPLY = "rack.after_reply"
RACK_RESPONSE_FINISHED = "rack.response_finished"
PUMA_SOCKET = "puma.socket"
PUMA_CONFIG = "puma.config"
PUMA_PEERCERT = "puma.peercert"
Expand Down
24 changes: 18 additions & 6 deletions lib/puma/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def handle_request(client, requests)
io_buffer = client.io_buffer
socket = client.io # io may be a MiniSSL::Socket
app_body = nil
error = nil

return false if closed_socket?(socket)

Expand Down Expand Up @@ -92,6 +93,7 @@ def handle_request(client, requests)
# array, we will invoke them when the request is done.
#
env[RACK_AFTER_REPLY] ||= []
env[RACK_RESPONSE_FINISHED] ||= []

begin
if @supported_http_methods == :any || @supported_http_methods.key?(env[REQUEST_METHOD])
Expand Down Expand Up @@ -119,15 +121,15 @@ def handle_request(client, requests)

return :async
end
rescue ThreadPool::ForceShutdown => e
@log_writer.unknown_error e, client, "Rack app"
rescue ThreadPool::ForceShutdown => error
@log_writer.unknown_error error, client, "Rack app"
@log_writer.log "Detected force shutdown of a thread"

status, headers, res_body = lowlevel_error(e, env, 503)
rescue Exception => e
@log_writer.unknown_error e, client, "Rack app"
status, headers, res_body = lowlevel_error(error, env, 503)
rescue Exception => error
@log_writer.unknown_error error, client, "Rack app"

status, headers, res_body = lowlevel_error(e, env, 500)
status, headers, res_body = lowlevel_error(error, env, 500)
end
prepare_response(status, headers, res_body, requests, client)
ensure
Expand All @@ -144,6 +146,16 @@ def handle_request(client, requests)
end
end
end

if response_finished = env[RACK_RESPONSE_FINISHED]
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using assignment within a conditional expression can be confusing. Consider using if (response_finished = env[RACK_RESPONSE_FINISHED]) with explicit parentheses to clarify intent, or extract the assignment to a separate line for better readability.

Suggested change
if response_finished = env[RACK_RESPONSE_FINISHED]
response_finished = env[RACK_RESPONSE_FINISHED]
if response_finished

Copilot uses AI. Check for mistakes.
response_finished.reverse_each do |o|
begin
o.call(env, status, headers, error)
rescue StandardError => e
@log_writer.debug_error e
end
end
end
end

# Assembles the headers and prepares the body for actually sending the
Expand Down
54 changes: 53 additions & 1 deletion test/test_rack_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def call(env)

def setup
@simple = lambda { |env| [200, { "x-header" => "Works" }, ["Hello"]] }
@server = Puma::Server.new @simple
@server = Puma::Server.new @simple, nil, log_writer: Puma::LogWriter.null
@port = (@server.add_tcp_listener HOST, 0).addr[1]
@tcp = "http://#{HOST}:#{@port}"
@stopped = false
Expand Down Expand Up @@ -196,6 +196,58 @@ def test_after_reply_exception
stop
end

def test_rack_response_finished
calls = []

@server.app = lambda do |env|
env['rack.response_finished'] << lambda { |c_env, status, headers, error|
calls << 1
assert_same env, c_env
assert_equal 200, status
assert_instance_of Hash, headers
assert_nil error
}
env['rack.response_finished'] << lambda { |env, status, headers, error| calls << 2; raise "Oops" }
env['rack.response_finished'] << lambda { |env, status, headers, error| calls << 3 }
@simple.call(env)
end


Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line should be removed to maintain consistent spacing in the test file.

Suggested change

Copilot uses AI. Check for mistakes.
@server.run

hit(["#{@tcp}/test"])

stop

assert_equal [3, 2, 1], calls
end

def test_rack_response_finished_on_error
calls = []

@server.app = lambda do |env|
env['rack.response_finished'] << lambda { |c_env, status, headers, error|
begin
assert_same env, c_env
assert_equal 500, status
assert_instance_of Hash, headers
assert_instance_of RuntimeError, error
assert_equal "test_rack_response_finished_on_error", error.message
calls << 1
end
}
raise "test_rack_response_finished_on_error"
end

@server.run

hit(["#{@tcp}/test"])

stop

assert_equal [1], calls
end

def test_rack_body_proxy
closed = false
body = Rack::BodyProxy.new(["Hello"]) { closed = true }
Expand Down