Skip to content

Commit

Permalink
Closes puma#2341 - Make sure fallback Rack response are sent to the c…
Browse files Browse the repository at this point in the history
…lient

Currently, when Client I/O operations raise any exceptions, we build a fallback rack response (either by lowlevel_error_handler or by default puma's hard-coded response), but doesn't do anything with it.
This commit ensures that this response is sent back to client.
Use `bundle exec puma -C test/config/lowlevel_error_handler.rb test/rackup/hello.ru` to see the behavior.
  • Loading branch information
Vuta committed Mar 15, 2023
1 parent c8be369 commit 70ce137
Show file tree
Hide file tree
Showing 5 changed files with 37 additions and 9 deletions.
18 changes: 12 additions & 6 deletions lib/puma/server.rb
Expand Up @@ -464,7 +464,7 @@ def process_client(client)
end
true
rescue StandardError => e
client_error(e, client)
client_error(e, client, requests)
# The ensure tries to close +client+ down
requests > 0
ensure
Expand Down Expand Up @@ -492,22 +492,21 @@ def with_force_shutdown(client, &block)
# :nocov:

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

lowlevel_error(e, client.env)
case e
when MiniSSL::SSLError
@log_writer.ssl_error e, client.io
when HttpParserError
client.write_error(400)
response_to_error(client, requests, e, 400)
@log_writer.parse_error e, client
when HttpParserError501
client.write_error(501)
response_to_error(client, requests, e, 501)
@log_writer.parse_error e, client
else
client.write_error(500)
response_to_error(client, requests, e, 500)
@log_writer.unknown_error e, nil, "Read"
end
end
Expand All @@ -533,6 +532,13 @@ def lowlevel_error(e, env, status=500)
end
end

def response_to_error(client, requests, err, status_code)
status, headers, res_body = lowlevel_error(err, client.env, status_code)
prepare_response(status, headers, res_body, requests, client)
client.write_error(status_code)
end
private :response_to_error

# Wait for all outstanding requests to finish.
#
def graceful_shutdown
Expand Down
7 changes: 7 additions & 0 deletions test/config/lowlevel_error_handler.rb
@@ -0,0 +1,7 @@
app do |env|
[200, nil, []]
end

lowlevel_error_handler do |err|
[500, {}, ["something wrong happened"]]
end
2 changes: 1 addition & 1 deletion test/test_config.rb
Expand Up @@ -586,7 +586,7 @@ def assert_run_hooks(hook_name, options = {})

def assert_warning_for_hooks_defined_in_single_mode(hook_name)
out, _ = capture_io do
conf = Puma::Configuration.new do |c|
Puma::Configuration.new do |c|
c.send(hook_name)
end
end
Expand Down
15 changes: 15 additions & 0 deletions test/test_puma_server.rb
Expand Up @@ -1545,4 +1545,19 @@ def test_streaming_enum_body_2
assert_equal str * loops, resp_body
assert_operator times.last - times.first, :>, 1.0
end

def test_lowlevel_error_handler_response
options = {
lowlevel_error_handler: ->(_error) do
[500, {}, ["something wrong happened"]]
end
}
broken_app = ->(_env) { [200, nil, []] }

server_run(**options, &broken_app)

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

assert_match(/something wrong happened/, data)
end
end
4 changes: 2 additions & 2 deletions test/test_response_header.rb
Expand Up @@ -49,15 +49,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.1 500 Internal Server Error/, data)
assert_match(/Puma caught this 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.1 500 Internal Server Error/, data)
assert_match(/Puma caught this error/, data)
end

# The values of the header must be Strings
Expand Down

0 comments on commit 70ce137

Please sign in to comment.