Compare commits

..

No commits in common. "b1c59c45ebe3dd24b8192d85a08288e96ab04ced" and "84f5b303f268f721d0f98d2864544715dc4abc67" have entirely different histories.

16 changed files with 37 additions and 114 deletions

View File

@ -1,7 +1,7 @@
version: '3'
services:
httpx:
image: ruby:3.2
image: ruby:3.1
environment:
- HTTPBIN_COALESCING_HOST=another
- HTTPX_RESOLVER_URI=https://doh/dns-query

View File

@ -45,12 +45,10 @@ module HTTPX
def_delegator :@write_buffer, :empty?
attr_reader :type, :io, :origin, :origins, :state, :pending, :options
attr_reader :io, :origin, :origins, :state, :pending, :options
attr_writer :timers
attr_accessor :family
def initialize(type, uri, options)
@type = type
@origins = [uri.origin]
@ -78,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)
@ -116,10 +121,7 @@ module HTTPX
return false unless connection.addresses
(
(open? && @origin == connection.origin) ||
!(@io.addresses & connection.addresses).empty?
) && @options == connection.options
!(@io.addresses & connection.addresses).empty? && @options == connection.options
end
# coalescable connections need to be mergeable!
@ -224,14 +226,6 @@ module HTTPX
@parser.close if @parser
end
# bypasses the state machine to force closing of connections still connecting.
# **only** used for Happy Eyeballs v2.
def force_reset
@state = :closing
transition(:closed)
emit(:close)
end
def reset
transition(:closing)
transition(:closed)
@ -533,12 +527,11 @@ module HTTPX
Errno::EINVAL,
Errno::ENETUNREACH,
Errno::EPIPE,
Errno::ENOENT,
SocketError => e
Errno::ENOENT => e
# connect errors, exit gracefully
error = ConnectionError.new(e.message)
error.set_backtrace(e.backtrace)
connecting? && callbacks(:connect_error).any? ? emit(:connect_error, error) : handle_error(error)
handle_error(error)
@state = :closed
emit(:close)
rescue TLSError => e
@ -557,8 +550,6 @@ module HTTPX
return if @state == :closed
@io.connect
emit(:tcp_open) if @io.state == :connected
return unless @io.connected?
@connected_at = Utils.now

View File

@ -160,8 +160,6 @@ module HTTPX
module URIExtensions
# uri 0.11 backport, ships with ruby 3.1
refine URI::Generic do
public :set_host
def non_ascii_hostname
@non_ascii_hostname
end

View File

@ -76,7 +76,11 @@ module HTTPX
Errno::EADDRNOTAVAIL,
Errno::EHOSTUNREACH,
SocketError => e
raise e if @ip_index <= 0
if @ip_index <= 0
error = ConnectionError.new(e.message)
error.set_backtrace(e.backtrace)
raise error
end
log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
@ip_index -= 1

View File

@ -72,7 +72,7 @@ module HTTPX
end
def init_connection(connection, _options)
resolve_connection(connection) unless connection.family
resolve_connection(connection)
connection.timers = @timers
connection.on(:open) do
@connected_connections += 1
@ -116,55 +116,18 @@ module HTTPX
# resolve a name (not the same as name being an IP, yet)
# 2. when the connection is initialized with an external already open IO.
#
connection.once(:connect_error, &connection.method(:handle_error))
on_resolver_connection(connection)
return
end
find_resolver_for(connection) do |resolver|
resolver << try_clone_connection(connection, resolver.family)
resolver << connection
next if resolver.empty?
select_connection(resolver)
end
end
def try_clone_connection(connection, family)
connection.family ||= family
if connection.family == family
return connection
end
new_connection = connection.class.new(connection.type, connection.origin, connection.options)
new_connection.family = family
connection.once(:tcp_open, &new_connection.method(:force_reset))
connection.once(:connect_error) do |err|
if new_connection.connecting?
new_connection.merge(connection)
else
connection.handle_error(err)
end
end
new_connection.once(:tcp_open) do
new_connection.merge(connection)
connection.force_reset
end
new_connection.once(:connect_error) do |err|
if connection.connecting?
# main connection has the requests
connection.merge(new_connection)
else
new_connection.handle_error(err)
end
end
init_connection(new_connection, connection.options)
new_connection
end
def on_resolver_connection(connection)
@connections << connection unless @connections.include?(connection)
found_connection = @connections.find do |ch|

View File

@ -108,15 +108,7 @@ module HTTPX
def decode_dns_answer(payload)
message = Resolv::DNS::Message.decode(payload)
# no domain was found
return if message.rcode == Resolv::DNS::RCode::NXDomain
addresses = []
# TODO: raise an "other dns OtherResolvError" type of error
return addresses if message.rcode != Resolv::DNS::RCode::NoError
message.each_answer do |question, _, value|
case value
when Resolv::DNS::Resource::IN::CNAME

View File

@ -136,22 +136,9 @@ module HTTPX
emit_resolve_error(connection, connection.origin.host, e)
return
end
if answers.nil?
# Indicates no such domain was found.
if answers.nil? || answers.empty?
host = @requests.delete(request)
connection = @queries.delete(host)
emit_resolve_error(connection) unless @queries.value?(connection)
elsif answers.empty?
# no address found, eliminate candidates
host = @requests.delete(request)
connection = @queries.delete(host)
# eliminate other candidates
@queries.delete_if { |_, conn| connection == conn }
emit_resolve_error(connection)
return

View File

@ -215,8 +215,7 @@ module HTTPX
raise ex
end
if addresses.nil?
# Indicates no such domain was found.
if addresses.nil? || addresses.empty?
hostname, connection = @queries.first
@queries.delete(hostname)
@timeouts.delete(hostname)
@ -225,14 +224,6 @@ module HTTPX
@connections.delete(connection)
raise NativeResolveError.new(connection, connection.origin.host)
end
elsif addresses.empty?
# no address found, eliminate candidates
_, connection = @queries.first
candidates = @queries.select { |_, conn| connection == conn }.keys
@queries.delete_if { |hs, _| candidates.include?(hs) }
@timeouts.delete_if { |hs, _| candidates.include?(hs) }
@connections.delete(connection)
raise NativeResolveError.new(connection, connection.origin.host)
else
address = addresses.first
name = address["name"]
@ -319,9 +310,7 @@ module HTTPX
ip, port = @nameserver[@ns_index]
port ||= DNS_PORT
uri = URI::Generic.build(scheme: "udp", port: port)
# uri.hostname = ip
# link-local IPv6 address may have a zone identifier, but URI does not support that yet.
uri.set_host(ip)
uri.hostname = ip
type = IO.registry(uri.scheme)
log { "resolver: server: #{uri}..." }
@io = type.new(uri, [IPAddr.new(ip)], @options)

View File

@ -73,6 +73,11 @@ module HTTPX
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)

View File

@ -21,7 +21,6 @@ module HTTPX
BUFFER_SIZE: Integer
attr_reader type: io_type
attr_reader origin: URI::Generic
attr_reader origins: Array[String]
attr_reader state: Symbol
@ -29,8 +28,7 @@ module HTTPX
attr_reader options: Options
attr_writer timers: Timers
attr_accessor family: Integer?
@type: io_type
@window_size: Integer
@read_buffer: Buffer
@write_buffer: Buffer
@ -38,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

View File

@ -22,9 +22,7 @@ module HTTPX
private
def initialize: () -> void
def try_clone_connection: (Connection connection, Integer? family) -> Connection
def initialize: () -> untyped
def resolve_connection: (Connection) -> void

View File

@ -30,6 +30,6 @@ module HTTPX
def self?.encode_dns_query: (String hostname, ?type: dns_resource) -> String
def self?.decode_dns_answer: (String) -> Array[dns_result]?
def self?.decode_dns_answer: (String) -> Array[dns_result]
end
end

View File

@ -6,8 +6,7 @@ module HTTPX
DEFAULTS: Hash[Symbol, untyped]
FAMILY_TYPES: Hash[singleton(Resolv::DNS::Resource), String]
attr_reader family: ip_family
@family: ip_family
@options: Options
@requests: Hash[Request, String]
@connections: Array[Connection]
@ -34,7 +33,7 @@ module HTTPX
def build_request: (String hostname) -> Request
def decode_response_body: (Response) -> Array[dns_result]?
def decode_response_body: (Response) -> Array[dns_result]
end
end
end

View File

@ -7,8 +7,7 @@ module HTTPX
DEFAULTS: Hash[Symbol, untyped]
DNS_PORT: Integer
attr_reader family: ip_family
@family: ip_family
@options: Options
@ns_index: Integer
@nameserver: Array[String]?

View File

@ -6,7 +6,7 @@ module HTTPX
RECORD_TYPES: Hash[Integer, singleton(Resolv::DNS::Resource)]
attr_reader family: ip_family?
attr_reader family: ip_family
@record_type: singleton(Resolv::DNS::Resource)
@options: Options
@ -24,8 +24,6 @@ module HTTPX
private
def emit_resolved_connection: (Connection connection, Array[IPAddr] addresses) -> void
def initialize: (ip_family? family, options options) -> void
def early_resolve: (Connection connection, ?hostname: String) -> void

View File

@ -23,7 +23,7 @@ module Requests
return unless uri.start_with?("http://")
response = HTTPX.get(uri, addresses: [EHOSTUNREACH_HOST] * 2)
verify_error_response(response, /No route to host/)
verify_error_response(response, Errno::EHOSTUNREACH)
end
# TODO: reset this test once it's possible to test ETIMEDOUT again