From c9527f962def004be9878f9377dc8f0cbcccc258 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Tue, 20 Dec 2022 18:25:37 +0000 Subject: [PATCH 1/2] finishing Happy Eyeballs v2 Until now, httpx was issuing concurrent DNS requests, but it'd only start connecting to the first, and then on the following by the right order, but sequentially. With this change, httpx will now continue the process by connecting concurrently to both IPv6 and IPv4, and close the other connection once one is established. This means both TCP and TLS (when applicable) need to succeed before the second connection is cancelled. --- lib/httpx/connection.rb | 7 +++++++ lib/httpx/pool.rb | 1 + lib/httpx/resolver/https.rb | 3 ++- lib/httpx/resolver/resolver.rb | 20 +++++++++++++------- sig/connection.rbs | 2 ++ 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/lib/httpx/connection.rb b/lib/httpx/connection.rb index f824db13..4cb15b21 100644 --- a/lib/httpx/connection.rb +++ b/lib/httpx/connection.rb @@ -76,6 +76,13 @@ module HTTPX self.addresses = @options.addresses if @options.addresses end + def clone_new_connection + new_conn = self.class.new(@type, @origin, @options) + once(:open, &new_conn.method(:reset)) + new_conn.once(:open, &method(:close)) + new_conn + end + # this is a semi-private method, to be used by the resolver # to initiate the io object. def addresses=(addrs) diff --git a/lib/httpx/pool.rb b/lib/httpx/pool.rb index 0ef0eed0..74e5f829 100644 --- a/lib/httpx/pool.rb +++ b/lib/httpx/pool.rb @@ -129,6 +129,7 @@ module HTTPX end def on_resolver_connection(connection) + @connections << connection unless @connections.include?(connection) found_connection = @connections.find do |ch| ch != connection && ch.mergeable?(connection) end diff --git a/lib/httpx/resolver/https.rb b/lib/httpx/resolver/https.rb index 55d1f85e..bc58c8de 100644 --- a/lib/httpx/resolver/https.rb +++ b/lib/httpx/resolver/https.rb @@ -69,7 +69,8 @@ module HTTPX @building_connection = true connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] })) @pool.init_connection(connection, @options) - emit_addresses(connection, @family, @uri_addresses) + # only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP. + emit_addresses(connection, @family, @uri_addresses) unless connection.addresses @building_connection = false connection end diff --git a/lib/httpx/resolver/resolver.rb b/lib/httpx/resolver/resolver.rb index 8e36a2d4..7f961148 100644 --- a/lib/httpx/resolver/resolver.rb +++ b/lib/httpx/resolver/resolver.rb @@ -63,20 +63,26 @@ module HTTPX log { "resolver: A response, applying resolution delay..." } @pool.after(0.05) do # double emission check - unless connection.addresses && addresses.intersect?(connection.addresses) - - connection.addresses = addresses - emit(:resolve, connection) - end + emit_resolved_connection(connection, addresses) unless connection.addresses && addresses.intersect?(connection.addresses) end else - connection.addresses = addresses - emit(:resolve, connection) + emit_resolved_connection(connection, addresses) end end private + def emit_resolved_connection(connection, addresses) + if connection.io && connection.connecting? && @pool + new_connection = connection.clone_new_connection + @pool.init_connection(new_connection, connection.options) + connection = new_connection + end + connection.addresses = addresses + + emit(:resolve, connection) + end + def early_resolve(connection, hostname: connection.origin.host) addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname)) diff --git a/sig/connection.rbs b/sig/connection.rbs index 374df35d..dcc13a36 100644 --- a/sig/connection.rbs +++ b/sig/connection.rbs @@ -36,6 +36,8 @@ module HTTPX @keep_alive_timeout: Numeric? @total_timeout: Numeric? + def clone_new_connection: () -> instance + def addresses: () -> Array[ipaddr]? def addresses=: (Array[ipaddr]) -> void From 1070cfa0aec3d7e94004dda1c20240dce7dfc568 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Wed, 21 Dec 2022 20:06:29 +0000 Subject: [PATCH 2/2] limiting spy version due to regression --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 0eaf93ca..baf73735 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ group :test do gem "minitest-proveit" gem "ruby-ntlm" gem "sentry-ruby" if RUBY_VERSION >= "2.4.0" - gem "spy" + gem "spy", "< 1.0.4" # TODO: remove this once upstream fixes bug if RUBY_VERSION < "2.3.0" gem "webmock", "< 3.15.0" else