diff options
-rw-r--r-- | lib/unicorn/configurator.rb | 9 | ||||
-rw-r--r-- | lib/unicorn/http_server.rb | 31 | ||||
-rw-r--r-- | test/unit/test_server.rb | 30 |
3 files changed, 68 insertions, 2 deletions
diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb index c3a4f2d..b0606af 100644 --- a/lib/unicorn/configurator.rb +++ b/lib/unicorn/configurator.rb @@ -276,6 +276,15 @@ class Unicorn::Configurator set_bool(:default_middleware, bool) end + # sets whether to enable the proposed early hints Rack API. + # If enabled, Rails 5.2+ will automatically send a 103 Early Hint + # for all the `javascript_include_tag` and `stylesheet_link_tag` + # in your response. See: https://api.rubyonrails.org/v5.2/classes/ActionDispatch/Request.html#method-i-send_early_hints + # See also https://tools.ietf.org/html/rfc8297 + 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..05dad99 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,25 @@ class Unicorn::HttpServer 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| + next if !vs || vs.empty? + values = vs.to_s.split("\n".freeze) + values.each do |v| + response << "#{k}: #{v}\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 +621,15 @@ class Unicorn::HttpServer # 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 @@ class TestHandler 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 @@ class WebServerTest < Test::Unit::TestCase 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" } |