From: Jean Boussier <jean.boussier@shopify.com>
To: unicorn-public@yhbt.net
Subject: [PATCH] Add early hints support
Date: Thu, 16 Jul 2020 12:05:38 +0200 [thread overview]
Message-ID: <058BA238-BEB3-4E54-9DE6-DC59BCB86246@shopify.com> (raw)
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
next reply other threads:[~2020-07-16 10:05 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-07-16 10:05 Jean Boussier [this message]
2020-07-16 10:50 ` [PATCH] Add early hints support Eric Wong
2020-07-16 11:41 ` Jean Boussier
2020-07-16 12:16 ` Eric Wong
2020-07-16 12:24 ` Jean Boussier
2020-07-17 1:19 ` Eric Wong
2020-07-20 9:18 ` Jean Boussier
2020-07-20 10:09 ` Eric Wong
2020-07-20 10:27 ` Jean Boussier
2020-07-20 10:55 ` Eric Wong
2020-07-20 11:53 ` Jean Boussier
2020-07-20 20:27 ` Eric Wong
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://yhbt.net/unicorn/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=058BA238-BEB3-4E54-9DE6-DC59BCB86246@shopify.com \
--to=jean.boussier@shopify.com \
--cc=unicorn-public@yhbt.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://yhbt.net/unicorn.git/
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).