From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on dcvr.yhbt.net X-Spam-Level: X-Spam-Status: No, score=-2.0 required=3.0 tests=BAYES_00,DKIMWL_WL_HIGH, DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,DKIM_VALID_EF,NUMERIC_HTTP_ADDR, RCVD_IN_DNSWL_NONE,SPF_HELO_NONE,SPF_PASS shortcircuit=no autolearn=ham autolearn_force=no version=3.4.2 Received: from mail-wr1-x42a.google.com (mail-wr1-x42a.google.com [IPv6:2a00:1450:4864:20::42a]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) by dcvr.yhbt.net (Postfix) with ESMTPS id E52381F5AE for ; Thu, 16 Jul 2020 11:41:39 +0000 (UTC) Received: by mail-wr1-x42a.google.com with SMTP id f2so6707903wrp.7 for ; Thu, 16 Jul 2020 04:41:39 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=shopify.com; s=google; h=mime-version:subject:from:in-reply-to:date:cc :content-transfer-encoding:message-id:references:to; bh=m22E11J2HwAJEzAKWR4WM9NrY9t5CfIrBLpX6EhYi6Q=; b=ah5tR/Vf5cGtpuGNpo45Hv6wou7QDAQH2TGmsrZwBoYpMi2TMfEiR+cN0gWGpSkJNM vrhws1xMyg0Kt3cED+t182MZdK+m/+1EJJ2VlsMsJgEcEck3srNl91oGt7Q6/6Hhr7ty 3rezgSc8/ZVBkR3Zyp94/W4YWLszLjcletvPc= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:mime-version:subject:from:in-reply-to:date:cc :content-transfer-encoding:message-id:references:to; bh=m22E11J2HwAJEzAKWR4WM9NrY9t5CfIrBLpX6EhYi6Q=; b=ZvW52900ZcqIoFbKTcPsmGc2JwMRTzT1EZ9GZT6bXSMs/ZizZb7zvOT5GqwPcLYfSC 4Esibd99vKCIMrWGw6SBLJwwo6jDWRCLebZz5ycZDinN9ZV8ZOgyGc5DtirnMGGJ+xiE beFnB4rSlmbnieNyRe4dDPpo8hZIFHhgKa/Xd+JX0AdGa97nCdtPRg1T6v0DgH31IAaW j5lXgcbeZK9FX4D9F0NNmA8ziL/SCN1GydvfB+LjVMZr6geerNme1F30Z4wi/E2SVtd8 VoO7mSbS50SoB6+v6NDfn1CVMPbS+2H3WBB+SSWmpUKaSPduoSbNL+PPjzvSrrZLPPPI u6Ug== X-Gm-Message-State: AOAM532cyS6ELmMCHX8M5AI/fUlDc1gNfqLqhF92waFXbnitWGrkalAI 2z36AgW5WdTZFtUw4suLDXQFmTaCXUs= X-Google-Smtp-Source: ABdhPJz07flihM3SQafo3114rCfhQ5J5OXBU3mU8erUF5p0xsfWaM9tM0o4/Z42xkuNeI6lpeg34IQ== X-Received: by 2002:a5d:420e:: with SMTP id n14mr4994583wrq.164.1594899698440; Thu, 16 Jul 2020 04:41:38 -0700 (PDT) Received: from [192.168.0.12] (87-231-62-161.rev.numericable.fr. [87.231.62.161]) by smtp.gmail.com with ESMTPSA id v3sm8714263wrq.57.2020.07.16.04.41.37 (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); Thu, 16 Jul 2020 04:41:37 -0700 (PDT) Content-Type: text/plain; charset=us-ascii Mime-Version: 1.0 (Mac OS X Mail 13.4 \(3608.80.23.2.2\)) Subject: Re: [PATCH] Add early hints support From: Jean Boussier In-Reply-To: <20200716105037.GA26605@dcvr> Date: Thu, 16 Jul 2020 13:41:36 +0200 Cc: unicorn-public@yhbt.net Content-Transfer-Encoding: quoted-printable Message-Id: <242F0859-0F83-4F14-A0FF-5BE392BB01E6@shopify.com> References: <058BA238-BEB3-4E54-9DE6-DC59BCB86246@shopify.com> <20200716105037.GA26605@dcvr> To: Eric Wong X-Mailer: Apple Mail (2.3608.80.23.2.2) List-Id: Thanks for the very timely response. > Since this RDoc ends up on the website, links to any relevant > Rails documentation and RFC 8297 would also be appropriate. > Otherwise non-Rails users like me might have no clue what > it's for. I updated the documentation, let me know what you think of it. > Are the method calls for .to_s necessary? I don't think they are, I mostly took inspiration from the puma = implementation that does all this defensive checks. Based on how that interface is used by Rails, we could assume both keys and values are strings already. I simplified the implementation. > Eep, extra branch... What's the performance impact for existing > users when not activated? (on Unix sockets). Extremely small. >=20 > Perhaps bypassing the method and accessing the @early_hints ivar > directly can be slightly faster w/o method dispatch. That > should also allow using attr_writer instead of attr_accessor, > I think. attr_reader is very optimized in MRI, it's barely slower than = @early_hints. Also it ensure that it doesn't emit a warning in verbose mode if the = variable isn't initialized. =46rom e0494e10de6549d1b513eef03e68bfa58a6b26ec Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Thu, 16 Jul 2020 11:39:30 +0200 Subject: [PATCH] Add early hints support 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 | 9 +++++++++ lib/unicorn/http_server.rb | 31 +++++++++++++++++++++++++++++-- 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 @@ def default_middleware(bool) set_bool(:default_middleware, bool) end =20 + # 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#metho= d-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 =20 attr_reader :pid, :logger @@ -588,6 +588,25 @@ def handle_error(client, e) rescue end =20 + def e103_response_write(client, headers) + response =3D 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 =3D 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 @@ 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 =3D @app.call(env =3D @request.read(client)) + env =3D @request.read(client) + + if early_hints + env["rack.early_hints"] =3D lambda do |headers| + e103_response_write(client, headers) + end + end + + status, headers, body =3D @app.call(env) =20 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 =20 +class TestEarlyHintsHandler + def call(env) + while env['rack.input'].read(4096) + end + env['rack.early_hints'].call( + "Link" =3D> "; rel=3Dpreload; as=3Dstyle\n;= rel=3Dpreload" + ) + [200, { 'Content-Type' =3D> 'text/plain' }, ['hello!\n']] + end +end =20 class WebServerTest < Test::Unit::TestCase =20 @@ -84,6 +94,26 @@ def test_preload_app_config tmp.close! end =20 + def test_early_hints + teardown + redirect_test_io do + @server =3D HttpServer.new(TestEarlyHintsHandler.new, + :listeners =3D> [ "127.0.0.1:#@port"], + :early_hints =3D> true) + @server.start + end + + sock =3D TCPSocket.new('127.0.0.1', @port) + sock.syswrite("GET / HTTP/1.0\r\n\r\n") + + responses =3D sock.sysread(4096) + assert_match %r{\AHTTP/1.[01] 103\b}, responses + assert_match %r{^Link: }, responses + assert_match %r{^Link: }, responses + + assert_match %r{^HTTP/1.[01] 200\b}, responses + end + def test_broken_app teardown app =3D lambda { |env| raise RuntimeError, "hello" } --=20 2.26.2