While not part of the rack spec, this API is exposed
by both puma and falcon, and Rails use it when available.
The 103 Early Hints response code is specified in RFC 8297.
---
lib/unicorn/configurator.rb | 5 +++++
lib/unicorn/http_server.rb | 33 +++++++++++++++++++++++++++++++--
test/unit/test_server.rb | 30 ++++++++++++++++++++++++++++++
3 files changed, 66 insertions(+), 2 deletions(-)
diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb
index c3a4f2d..43e91f4 100644
--- a/lib/unicorn/configurator.rb
+++ b/lib/unicorn/configurator.rb
@@ -276,6 +276,11 @@ def default_middleware(bool)
set_bool(:default_middleware, bool)
end
+ # sets wether to enable rack's early hints API.
+ def early_hints(bool)
+ set_bool(:early_hints, bool)
+ end
+
# sets listeners to the given +addresses+, replacing or augmenting the
# current set. This is for the global listener pool shared by all
# worker processes. For per-worker listeners, see the after_fork example
diff --git a/lib/unicorn/http_server.rb b/lib/unicorn/http_server.rb
index 45a2e97..3e0c9a4 100644
--- a/lib/unicorn/http_server.rb
+++ b/lib/unicorn/http_server.rb
@@ -15,7 +15,7 @@ class Unicorn::HttpServer
:before_fork, :after_fork, :before_exec,
:listener_opts, :preload_app,
:orig_app, :config, :ready_pipe, :user,
- :default_middleware
+ :default_middleware, :early_hints
attr_writer :after_worker_exit, :after_worker_ready, :worker_exec
attr_reader :pid, :logger
@@ -588,6 +588,27 @@ def handle_error(client, e)
rescue
end
+ def e103_response_write(client, headers)
+ response = if @request.response_start_sent
+ "103 Early Hints\r\n"
+ else
+ "HTTP/1.1 103 Early Hints\r\n"
+ end
+
+ headers.each_pair do |k, vs|
+ if vs.respond_to?(:to_s) && !vs.to_s.empty?
+ vs.to_s.split("\n".freeze).each do |v|
+ response << "#{k}: #{v}\r\n"
+ end
+ else
+ response << "#{k}: #{vs}\r\n"
+ end
+ end
+ response << "\r\n".freeze
+ response << "HTTP/1.1 ".freeze if @request.response_start_sent
+ client.write(response)
+ end
+
def e100_response_write(client, env)
# We use String#freeze to avoid allocations under Ruby 2.1+
# Not many users hit this code path, so it's better to reduce the
@@ -602,7 +623,15 @@ def e100_response_write(client, env)
# once a client is accepted, it is processed in its entirety here
# in 3 easy steps: read request, call app, write app response
def process_client(client)
- status, headers, body = @app.call(env = @request.read(client))
+ env = @request.read(client)
+
+ if early_hints
+ env["rack.early_hints"] = lambda do |headers|
+ e103_response_write(client, headers)
+ end
+ end
+
+ status, headers, body = @app.call(env)
begin
return if @request.hijacked?
diff --git a/test/unit/test_server.rb b/test/unit/test_server.rb
index 8096955..d706243 100644
--- a/test/unit/test_server.rb
+++ b/test/unit/test_server.rb
@@ -23,6 +23,16 @@ def call(env)
end
end
+class TestEarlyHintsHandler
+ def call(env)
+ while env['rack.input'].read(4096)
+ end
+ env['rack.early_hints'].call(
+ "Link" => "</style.css>; rel=preload; as=style\n</script.js>;
rel=preload"
+ )
+ [200, { 'Content-Type' => 'text/plain' }, ['hello!\n']]
+ end
+end
class WebServerTest < Test::Unit::TestCase
@@ -84,6 +94,26 @@ def test_preload_app_config
tmp.close!
end
+ def test_early_hints
+ teardown
+ redirect_test_io do
+ @server = HttpServer.new(TestEarlyHintsHandler.new,
+ :listeners => [ "127.0.0.1:#@port"],
+ :early_hints => true)
+ @server.start
+ end
+
+ sock = TCPSocket.new('127.0.0.1', @port)
+ sock.syswrite("GET / HTTP/1.0\r\n\r\n")
+
+ responses = sock.sysread(4096)
+ assert_match %r{\AHTTP/1.[01] 103\b}, responses
+ assert_match %r{^Link: </style\.css>}, responses
+ assert_match %r{^Link: </script\.js>}, responses
+
+ assert_match %r{^HTTP/1.[01] 200\b}, responses
+ end
+
def test_broken_app
teardown
app = lambda { |env| raise RuntimeError, "hello" }
--
2.26.2