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' version: '3'
services: services:
httpx: httpx:
image: ruby:3.2 image: ruby:3.1
environment: environment:
- HTTPBIN_COALESCING_HOST=another - HTTPBIN_COALESCING_HOST=another
- HTTPX_RESOLVER_URI=https://doh/dns-query - HTTPX_RESOLVER_URI=https://doh/dns-query

View File

@ -45,12 +45,10 @@ module HTTPX
def_delegator :@write_buffer, :empty? 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_writer :timers
attr_accessor :family
def initialize(type, uri, options) def initialize(type, uri, options)
@type = type @type = type
@origins = [uri.origin] @origins = [uri.origin]
@ -78,6 +76,13 @@ module HTTPX
self.addresses = @options.addresses if @options.addresses self.addresses = @options.addresses if @options.addresses
end 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 # this is a semi-private method, to be used by the resolver
# to initiate the io object. # to initiate the io object.
def addresses=(addrs) def addresses=(addrs)
@ -116,10 +121,7 @@ module HTTPX
return false unless connection.addresses return false unless connection.addresses
( !(@io.addresses & connection.addresses).empty? && @options == connection.options
(open? && @origin == connection.origin) ||
!(@io.addresses & connection.addresses).empty?
) && @options == connection.options
end end
# coalescable connections need to be mergeable! # coalescable connections need to be mergeable!
@ -224,14 +226,6 @@ module HTTPX
@parser.close if @parser @parser.close if @parser
end 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 def reset
transition(:closing) transition(:closing)
transition(:closed) transition(:closed)
@ -533,12 +527,11 @@ module HTTPX
Errno::EINVAL, Errno::EINVAL,
Errno::ENETUNREACH, Errno::ENETUNREACH,
Errno::EPIPE, Errno::EPIPE,
Errno::ENOENT, Errno::ENOENT => e
SocketError => e
# connect errors, exit gracefully # connect errors, exit gracefully
error = ConnectionError.new(e.message) error = ConnectionError.new(e.message)
error.set_backtrace(e.backtrace) error.set_backtrace(e.backtrace)
connecting? && callbacks(:connect_error).any? ? emit(:connect_error, error) : handle_error(error) handle_error(error)
@state = :closed @state = :closed
emit(:close) emit(:close)
rescue TLSError => e rescue TLSError => e
@ -557,8 +550,6 @@ module HTTPX
return if @state == :closed return if @state == :closed
@io.connect @io.connect
emit(:tcp_open) if @io.state == :connected
return unless @io.connected? return unless @io.connected?
@connected_at = Utils.now @connected_at = Utils.now

View File

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

View File

@ -76,7 +76,11 @@ module HTTPX
Errno::EADDRNOTAVAIL, Errno::EADDRNOTAVAIL,
Errno::EHOSTUNREACH, Errno::EHOSTUNREACH,
SocketError => e 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..." } log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
@ip_index -= 1 @ip_index -= 1

View File

@ -72,7 +72,7 @@ module HTTPX
end end
def init_connection(connection, _options) def init_connection(connection, _options)
resolve_connection(connection) unless connection.family resolve_connection(connection)
connection.timers = @timers connection.timers = @timers
connection.on(:open) do connection.on(:open) do
@connected_connections += 1 @connected_connections += 1
@ -116,55 +116,18 @@ module HTTPX
# resolve a name (not the same as name being an IP, yet) # resolve a name (not the same as name being an IP, yet)
# 2. when the connection is initialized with an external already open IO. # 2. when the connection is initialized with an external already open IO.
# #
connection.once(:connect_error, &connection.method(:handle_error))
on_resolver_connection(connection) on_resolver_connection(connection)
return return
end end
find_resolver_for(connection) do |resolver| find_resolver_for(connection) do |resolver|
resolver << try_clone_connection(connection, resolver.family) resolver << connection
next if resolver.empty? next if resolver.empty?
select_connection(resolver) select_connection(resolver)
end end
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) def on_resolver_connection(connection)
@connections << connection unless @connections.include?(connection) @connections << connection unless @connections.include?(connection)
found_connection = @connections.find do |ch| found_connection = @connections.find do |ch|

View File

@ -108,15 +108,7 @@ module HTTPX
def decode_dns_answer(payload) def decode_dns_answer(payload)
message = Resolv::DNS::Message.decode(payload) message = Resolv::DNS::Message.decode(payload)
# no domain was found
return if message.rcode == Resolv::DNS::RCode::NXDomain
addresses = [] 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| message.each_answer do |question, _, value|
case value case value
when Resolv::DNS::Resource::IN::CNAME when Resolv::DNS::Resource::IN::CNAME

View File

@ -136,22 +136,9 @@ module HTTPX
emit_resolve_error(connection, connection.origin.host, e) emit_resolve_error(connection, connection.origin.host, e)
return return
end end
if answers.nil? || answers.empty?
if answers.nil?
# Indicates no such domain was found.
host = @requests.delete(request) host = @requests.delete(request)
connection = @queries.delete(host) 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) emit_resolve_error(connection)
return return

View File

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

View File

@ -73,6 +73,11 @@ module HTTPX
private private
def emit_resolved_connection(connection, addresses) 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 connection.addresses = addresses
emit(:resolve, connection) emit(:resolve, connection)

View File

@ -21,7 +21,6 @@ module HTTPX
BUFFER_SIZE: Integer BUFFER_SIZE: Integer
attr_reader type: io_type
attr_reader origin: URI::Generic attr_reader origin: URI::Generic
attr_reader origins: Array[String] attr_reader origins: Array[String]
attr_reader state: Symbol attr_reader state: Symbol
@ -29,8 +28,7 @@ module HTTPX
attr_reader options: Options attr_reader options: Options
attr_writer timers: Timers attr_writer timers: Timers
attr_accessor family: Integer? @type: io_type
@window_size: Integer @window_size: Integer
@read_buffer: Buffer @read_buffer: Buffer
@write_buffer: Buffer @write_buffer: Buffer
@ -38,6 +36,8 @@ module HTTPX
@keep_alive_timeout: Numeric? @keep_alive_timeout: Numeric?
@total_timeout: Numeric? @total_timeout: Numeric?
def clone_new_connection: () -> instance
def addresses: () -> Array[ipaddr]? def addresses: () -> Array[ipaddr]?
def addresses=: (Array[ipaddr]) -> void def addresses=: (Array[ipaddr]) -> void

View File

@ -22,9 +22,7 @@ module HTTPX
private private
def initialize: () -> void def initialize: () -> untyped
def try_clone_connection: (Connection connection, Integer? family) -> Connection
def resolve_connection: (Connection) -> void 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?.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
end end

View File

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

View File

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

View File

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

View File

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