Merge branch 'gh-103' into 'master'

fix: do not check if multi-homed network at boot time, instead do it a runtime

See merge request os85/httpx!408
This commit is contained in:
HoneyryderChuck 2025-09-23 10:03:12 +00:00
commit 267bbea7f9
8 changed files with 48 additions and 28 deletions

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
require "socket"
module HTTPX
# Contains a set of options which are passed and shared across from session to its requests or
# responses.
@ -414,18 +412,6 @@ module HTTPX
end
end
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
ip_address_families = begin
list = Socket.ip_address_list
if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
[Socket::AF_INET6, Socket::AF_INET]
else
[Socket::AF_INET]
end
rescue NotImplementedError
[Socket::AF_INET]
end.freeze
DEFAULT_OPTIONS = {
:max_requests => Float::INFINITY,
:debug => nil,
@ -470,7 +456,7 @@ module HTTPX
:resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
:resolver_options => { cache: true }.freeze,
:pool_options => EMPTY_HASH,
:ip_families => ip_address_families,
:ip_families => nil,
:close_on_fork => false,
}.freeze
end

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true
require "socket"
require "resolv"
module HTTPX
@ -22,6 +23,20 @@ module HTTPX
module_function
def supported_ip_families
@supported_ip_families ||= begin
# https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
list = Socket.ip_address_list
if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
[Socket::AF_INET6, Socket::AF_INET]
else
[Socket::AF_INET]
end
rescue NotImplementedError
[Socket::AF_INET]
end.freeze
end
def resolver_for(resolver_type, options)
case resolver_type
when Symbol

View File

@ -15,7 +15,9 @@ module HTTPX
@options = options
@resolver_options = @options.resolver_options
@resolvers = options.ip_families.map do |ip_family|
ip_families = options.ip_families || Resolver.supported_ip_families
@resolvers = ip_families.map do |ip_family|
resolver = resolver_type.new(ip_family, options)
resolver.multi = self
resolver
@ -67,8 +69,12 @@ module HTTPX
addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
return false unless addresses
ip_families = connection.options.ip_families || Resolver.supported_ip_families
resolved = false
addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
next unless ip_families.include?(family)
# try to match the resolver by family. However, there are cases where that's not possible, as when
# the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
resolver = @resolvers.find { |r| r.family == family } || @resolvers.first
@ -85,7 +91,11 @@ module HTTPX
end
def lazy_resolve(connection)
ip_families = connection.options.ip_families || Resolver.supported_ip_families
@resolvers.each do |resolver|
next unless ip_families.include?(resolver.family)
resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
next if resolver.empty?

View File

@ -79,12 +79,18 @@ module HTTPX
"answer #{connection.peer.host}: #{addresses.inspect} (early resolve: #{early_resolve})"
end
if !early_resolve && # do not apply resolution delay for non-dns name resolution
@current_selector && # just in case...
family == Socket::AF_INET && # resolution delay only applies to IPv4
!connection.io && # connection already has addresses and initiated/ended handshake
connection.options.ip_families.size > 1 && # no need to delay if not supporting dual stack IP
addresses.first.to_s != connection.peer.host.to_s # connection URL host is already the IP (early resolve included perhaps?)
# do not apply resolution delay for non-dns name resolution
if !early_resolve &&
# just in case...
@current_selector &&
# resolution delay only applies to IPv4
family == Socket::AF_INET &&
# connection already has addresses and initiated/ended handshake
!connection.io &&
# no need to delay if not supporting dual stack / multi-homed IP
(connection.options.ip_families || Resolver.supported_ip_families).size > 1 &&
# connection URL host is already the IP (early resolve included perhaps?)
addresses.first.to_s != connection.peer.host.to_s
log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }
@current_selector.after(0.05) do

View File

@ -187,7 +187,9 @@ module HTTPX
transition(:open)
connection.options.ip_families.each do |family|
ip_families = connection.options.ip_families || Resolver.supported_ip_families
ip_families.each do |family|
@queries << [family, connection]
end
async_resolve(connection, hostname, scheme)
@ -195,7 +197,7 @@ module HTTPX
end
def async_resolve(connection, hostname, scheme)
families = connection.options.ip_families
families = connection.options.ip_families || Resolver.supported_ip_families
log { "resolver: query for #{hostname}" }
timeouts = @timeouts[connection.peer.host]
resolve_timeout = timeouts.first

View File

@ -130,7 +130,7 @@ module HTTPX
attr_reader pool_options: pool_options
# ip_families
attr_reader ip_families: Array[ip_family]
attr_reader ip_families: Array[ip_family]?
def ==: (Options other) -> bool
@ -195,7 +195,7 @@ module HTTPX
def option_addresses: (ipaddr | _ToAry[ipaddr] value) -> Array[ipaddr]
def option_ip_families: (Integer | _ToAry[Integer] value) -> Array[Integer]
def option_ip_families: (ip_family | _ToAry[ip_family] value) -> Array[ip_family]
end
type options = Options | Hash[Symbol, untyped]

View File

@ -23,6 +23,8 @@ module HTTPX
def self?.hosts_resolve: (String hostname) -> Array[Entry]?
def self?.supported_ip_families: () -> Array[ip_family]
def self?.resolver_for: (Symbol | singleton(Resolver) resolver_type, Options options) -> singleton(Resolver)
def self?.cached_lookup: (String hostname) -> Array[Entry]?

View File

@ -3,14 +3,13 @@
require_relative "test"
class ByIpCertServer < TestServer
USE_IPV6 = HTTPX::Options::DEFAULT_OPTIONS[:ip_families].size > 1
CERTS_DIR = File.expand_path("../ci/certs", __dir__)
def initialize
cert = OpenSSL::X509::Certificate.new(File.read(File.join(CERTS_DIR, "localhost-server.crt")))
key = OpenSSL::PKey.read(File.read(File.join(CERTS_DIR, "localhost-server.key")))
super(
:BindAddress => USE_IPV6 ? "::1" : "127.0.0.1",
:BindAddress => HTTPX::Resolver.supported_ip_families.size > 1 ? "::1" : "127.0.0.1",
:SSLEnable => true,
:SSLCertificate => cert,
:SSLPrivateKey => key,