Skip to content

Commit

Permalink
Add customizable error messages on thread shutdown
Browse files Browse the repository at this point in the history
  • Loading branch information
zanker-stripe committed Mar 16, 2020
1 parent f181938 commit bb0939e
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 3 deletions.
1 change: 1 addition & 0 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* `GC.compact` is called before fork if available (#2093)
* Add `requests_count` to workers stats. (#2106)
* Increases maximum URI path length from 2048 to 8196 bytes (#2167)
* Added `force_shutdown_error_response` to override the force shutdown response to clients. (#2182)

* Deprecations, Removals and Breaking API Changes
* `Puma.stats` now returns a Hash instead of a JSON string (#2086)
Expand Down
16 changes: 16 additions & 0 deletions lib/puma/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,22 @@ def lowlevel_error_handler(obj=nil, &block)
@options[:lowlevel_error_handler] = obj
end

# When force_shutdown_after is set, this allows you to control the message returned
# on a force shutdown to the client.
#
# @example
# force_shutdown_error_response(500, {"Content-Type" => "application/json"}, [JSON.generate({message: "Server shutdown."})])
def force_shutdown_error_response(status, headers, response)
raise "Headers must be a hash" unless headers.is_a?(Hash)
raise "Response must be enumerable" unless response.is_a?(Enumerable)

@options[:force_shutdown_error_response] = [
Integer(status),
headers,
response
]
end

# This option is used to allow your app and its gems to be
# properly reloaded when not using preload.
#
Expand Down
10 changes: 7 additions & 3 deletions lib/puma/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -593,9 +593,13 @@ def handle_request(req, lines)
@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"]
if @options[:force_shutdown_error_response]
status, headers, res_body = @options[:force_shutdown_error_response]
else
status = 503
headers = {}
res_body = ["Request was internally terminated early\n"]
end

rescue Exception => e
@events.unknown_error self, e, "Rack app", env
Expand Down
6 changes: 6 additions & 0 deletions test/config/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@
lowlevel_error_handler do |err|
[200, {}, ["error page"]]
end

force_shutdown_error_response(
500,
{"Content-Type" => "application/json"},
["{}"]
)
9 changes: 9 additions & 0 deletions test/test_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ def test_lowlevel_error_handler_DSL
assert_equal [200, {}, ["error page"]], app.call({})
end

def test_force_shutdown_error_response_DSL
conf = Puma::Configuration.new do |c|
c.load "test/config/app.rb"
end
conf.load

assert_equal [500, {"Content-Type" => "application/json"}, ["{}"]], conf.options[:force_shutdown_error_response]
end

def test_allow_users_to_override_default_options
conf = Puma::Configuration.new(restart_cmd: 'bin/rails server')

Expand Down
30 changes: 30 additions & 0 deletions test/test_puma_server.rb
Original file line number Diff line number Diff line change
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
shutdown_response = [500, {"Content-Type" => "application/json"}, ["{}\n"]]
@server = Puma::Server.new @app, @events, {:force_shutdown_error_response => shutdown_response, :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(/Request was internally terminated early\n$/, 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 Down

0 comments on commit bb0939e

Please sign in to comment.