Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Always send lowlevel_error response to client (#2731)" #2809

Merged
merged 1 commit into from Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
76 changes: 29 additions & 47 deletions lib/puma/request.rb
Expand Up @@ -46,7 +46,11 @@ def handle_request(client, lines, requests)
env[HIJACK_P] = true
env[HIJACK] = client

env[RACK_INPUT] = client.body
body = client.body

head = env[REQUEST_METHOD] == HEAD

env[RACK_INPUT] = body
env[RACK_URL_SCHEME] ||= default_server_port(env) == PORT_443 ? HTTPS : HTTP

if @early_hints
Expand All @@ -65,58 +69,36 @@ def handle_request(client, lines, requests)
# A rack extension. If the app writes #call'ables to this
# array, we will invoke them when the request is done.
#
env[RACK_AFTER_REPLY] = []
after_reply = env[RACK_AFTER_REPLY] = []

begin
status, headers, res_body = @thread_pool.with_force_shutdown do
@app.call(env)
end

return :async if client.hijacked

status = status.to_i

if status == -1
unless headers.empty? and res_body == []
raise "async response must have empty headers and body"
begin
status, headers, res_body = @thread_pool.with_force_shutdown do
@app.call(env)
end

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

status, headers, res_body = lowlevel_error(e, env, 503)
rescue Exception => e
@events.unknown_error e, client, "Rack app"
return :async if client.hijacked

status, headers, res_body = lowlevel_error(e, env, 500)
end
status = status.to_i

write_response(status, headers, res_body, lines, requests, client)
end
if status == -1
unless headers.empty? and res_body == []
raise "async response must have empty headers and body"
end

# Does the actual response writing for Request#handle_request and Server#client_error
#
# @param status [Integer] the status returned by the Rack application
# @param headers [Hash] the headers returned by the Rack application
# @param res_body [Array] the body returned by the Rack application
# @param lines [Puma::IOBuffer] modified in place
# @param requests [Integer] number of inline requests handled
# @param client [Puma::Client]
# @return [Boolean,:async]
def write_response(status, headers, res_body, lines, requests, client)
env = client.env
io = client.io
return :async
end
rescue ThreadPool::ForceShutdown => e
@events.unknown_error e, client, "Rack app"
@events.log "Detected force shutdown of a thread"

return false if closed_socket?(io)
lines.clear
status, headers, res_body = lowlevel_error(e, env, 503)
rescue Exception => e
@events.unknown_error e, client, "Rack app"

head = env[REQUEST_METHOD] == HEAD
after_reply = env[RACK_AFTER_REPLY] || []
status, headers, res_body = lowlevel_error(e, env, 500)
end

begin
res_info = {}
res_info[:content_length] = nil
res_info[:no_body] = head
Expand Down Expand Up @@ -167,9 +149,9 @@ def write_response(status, headers, res_body, lines, requests, client)
res_body.each do |part|
next if part.bytesize.zero?
if chunked
fast_write io, (part.bytesize.to_s(16) << line_ending)
fast_write io, part # part may have different encoding
fast_write io, line_ending
fast_write io, (part.bytesize.to_s(16) << line_ending)
fast_write io, part # part may have different encoding
fast_write io, line_ending
else
fast_write io, part
end
Expand All @@ -187,7 +169,7 @@ def write_response(status, headers, res_body, lines, requests, client)
ensure
uncork_socket io

client.body.close if client.body
body.close
client.tempfile.unlink if client.tempfile
res_body.close if res_body.respond_to? :close

Expand Down
20 changes: 9 additions & 11 deletions lib/puma/server.rb
Expand Up @@ -476,7 +476,7 @@ def process_client(client, buffer)
end
true
rescue StandardError => e
client_error(e, client, buffer, requests)
client_error(e, client)
# The ensure tries to close +client+ down
requests > 0
ensure
Expand Down Expand Up @@ -504,36 +504,34 @@ def with_force_shutdown(client, &block)
# :nocov:

# Handle various error types thrown by Client I/O operations.
def client_error(e, client, buffer = ::Puma::IOBuffer.new, requests = 1)
def client_error(e, client)
# Swallow, do not log
return if [ConnectionError, EOFError].include?(e.class)

lowlevel_error(e, client.env)
case e
when MiniSSL::SSLError
@events.ssl_error e, client.io
when HttpParserError
status, headers, res_body = lowlevel_error(e, client.env, 400)
write_response(status, headers, res_body, buffer, requests, client)
client.write_error(400)
@events.parse_error e, client
else
status, headers, res_body = lowlevel_error(e, client.env)
write_response(status, headers, res_body, buffer, requests, client)
client.write_error(500)
@events.unknown_error e, nil, "Read"
end
end

# A fallback rack response if +@app+ raises as exception.
#
def lowlevel_error(e, env, status = 500)
def lowlevel_error(e, env, status=500)
if handler = @options[:lowlevel_error_handler]
if handler.arity == 1
handler_status, headers, res_body = handler.call(e)
return handler.call(e)
elsif handler.arity == 2
handler_status, headers, res_body = handler.call(e, env)
return handler.call(e, env)
else
handler_status, headers, res_body = handler.call(e, env, status)
return handler.call(e, env, status)
end
return [handler_status || status, headers || {}, res_body || []]
end

if @leak_stack_on_error
Expand Down
11 changes: 0 additions & 11 deletions test/test_puma_server.rb
Expand Up @@ -363,17 +363,6 @@ def test_lowlevel_error_message
assert (data.size > 0), "Expected response message to be not empty"
end

def test_lowlevel_error_handler_custom_response
options = { lowlevel_error_handler: ->(_err) { [500, {}, ["error page"]] } }
# setting the headers argument to nil will trigger exception inside Puma
broken_app = ->(_env) { [200, nil, []] }
server_run(**options, &broken_app)

data = send_http_and_read "GET / HTTP/1.0\r\n\r\n"

assert_match %r{HTTP/1.0 500 Internal Server Error\r\nContent-Length: 10\r\n\r\nerror page}, data
end

def test_force_shutdown_error_default
server_run(force_shutdown_after: 2) do
@server.stop
Expand Down
4 changes: 2 additions & 2 deletions test/test_response_header.rb
Expand Up @@ -45,15 +45,15 @@ def test_integer_key
server_run app: ->(env) { [200, { 1 => 'Boo'}, []] }
data = send_http_and_read "GET / HTTP/1.0\r\n\r\n"

assert_match(/HTTP\/1.0 500 Internal Server Error/, data)
assert_match(/HTTP\/1.1 500 Internal Server Error/, data)
end

# The header must respond to each
def test_nil_header
server_run app: ->(env) { [200, nil, []] }
data = send_http_and_read "GET / HTTP/1.0\r\n\r\n"

assert_match(/HTTP\/1.0 500 Internal Server Error/, data)
assert_match(/HTTP\/1.1 500 Internal Server Error/, data)
end

# The values of the header must be Strings
Expand Down