Skip to content

Commit

Permalink
Handle thread shutdown responses via low level error messages
Browse files Browse the repository at this point in the history
  • Loading branch information
zanker-stripe committed Mar 26, 2020
1 parent 0b737cc commit ec06a2e
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 13 deletions.
1 change: 1 addition & 0 deletions History.md
Expand Up @@ -8,6 +8,7 @@
* Add `requests_count` to workers stats. (#2106)
* Increases maximum URI path length from 2048 to 8196 bytes (#2167)
* Sending SIGWINCH to any Puma worker now prints currently active threads and their backtraces (#2195)
* Force shutdown responses can be overridden by using the `lowlevel_error_handler` config ()

* Deprecations, Removals and Breaking API Changes
* `Puma.stats` now returns a Hash instead of a JSON string (#2086)
Expand Down
27 changes: 14 additions & 13 deletions lib/puma/server.rb
Expand Up @@ -589,18 +589,17 @@ def handle_request(req, lines)

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

status = 503
headers = {}
res_body = ["Request was internally terminated early\n"]

rescue Exception => e
@events.unknown_error self, e, "Rack app", env

status, headers, res_body = lowlevel_error(e, env)
status = 500
if e.is_a?(ThreadPool::ForceShutdown)
status = 503

@events.log "Detected force shutdown of a thread, returning 503"
end

status, headers, res_body = lowlevel_error(e, env, status)
end

content_length = nil
Expand Down Expand Up @@ -807,19 +806,21 @@ def read_body(env, client, body, cl)

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

if @leak_stack_on_error
[500, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{e.backtrace.join("\n")}"]]
[status, {}, ["Puma caught this error: #{e.message} (#{e.class})\n#{e.backtrace.join("\n")}"]]
else
[500, {}, ["An unhandled lowlevel error occurred. The application logs may have details.\n"]]
[status, {}, ["An unhandled lowlevel error occurred. The application logs may have details.\n"]]
end
end

Expand Down
45 changes: 45 additions & 0 deletions test/test_puma_server.rb
Expand Up @@ -261,6 +261,36 @@ def test_doesnt_print_backtrace_in_production
assert_match(/HTTP\/1.0 500 Internal Server Error/, data)
end

def test_force_shutdown_custom_error_message
handler = lambda {|err, env, status| [500, {"Content-Type" => "application/json"}, ["{}\n"]]}
@server = Puma::Server.new @app, @events, {:lowlevel_error_handler => handler, :force_shutdown_after => 2}

server_run app: ->(env) do
@server.stop
sleep 10
end

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(/Content-Type: application\/json/, data)
assert_match(/{}\n$/, data)
end

def test_force_shutdown_error_default
@server = Puma::Server.new @app, @events, {:force_shutdown_after => 2}

server_run app: ->(env) do
@server.stop
sleep 10
end

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

assert_match(/HTTP\/1.0 503 Service Unavailable/, data)
assert_match(/Puma caught this error.+Puma::ThreadPool::ForceShutdown/, data)
end

def test_prints_custom_error
re = lambda { |err| [302, {'Content-Type' => 'text', 'Location' => 'foo.html'}, ['302 found']] }
@server = Puma::Server.new @app, @events, {:lowlevel_error_handler => re}
Expand All @@ -287,6 +317,21 @@ def test_leh_gets_env_as_well
assert_match(/HTTP\/1.0 302 Found/, data)
end

def test_leh_has_status
re = lambda { |err, env, status|
raise "Cannot find status" unless status
[302, {'Content-Type' => 'text', 'Location' => 'foo.html'}, ['302 found']]
}

@server = Puma::Server.new @app, @events, {:lowlevel_error_handler => re}

server_run app: ->(env) { raise "don't leak me bro" }

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

assert_match(/HTTP\/1.0 302 Found/, data)
end

def test_custom_http_codes_10
server_run app: ->(env) { [449, {}, [""]] }

Expand Down

0 comments on commit ec06a2e

Please sign in to comment.