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]
response_finished.reverse_each do |o|
begin
o.call(env, status, headers, error)
rescue StandardError => e
@log_writer.debug_error e
end
end
end
Comment on lines +150 to +158
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

status and headers may be nil for early return paths.

If handle_request returns early at line 57 (closed_socket?) or line 60 (413 response), the status and headers local variables are never assigned. The callbacks would then receive nil for both parameters.

Notably, for the 413 case, a response is actually sent to the client, but the callback receives nil instead of the actual status (413).

Consider initializing status and headers before potential early returns, or updating them in those paths:

🛠️ Possible approach
     def handle_request(client, requests)
       env = client.env
       io_buffer = client.io_buffer
       socket  = client.io   # io may be a MiniSSL::Socket
       app_body = nil
       error = nil
+      status = nil
+      headers = nil
 
       return false if closed_socket?(socket)
 
       if client.http_content_length_limit_exceeded
+        status = 413
+        headers = {}
         return prepare_response(413, {}, ["Payload Too Large"], requests, client)
       end
🤖 Prompt for AI Agents
In `@lib/puma/request.rb` around lines 150 - 158, The callbacks in the
RACK_RESPONSE_FINISHED handling can receive nil because local variables status
and headers aren't set on early returns in handle_request (e.g., closed_socket?
and the 413 branch); ensure status and headers are initialized and updated on
those paths so callbacks get the real response values: initialize status and
headers before calling handle_request (or set them inside the closed_socket? and
413 branches) and propagate those values into the response_finished.each
callback invocation (the response_finished block that calls o.call(env, status,
headers, error)); update any early-return branches to assign the correct
numeric/status and header hash rather than leaving them nil.

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


@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