mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-07-14 00:00:49 -04:00
Compare commits
8 Commits
84f5b303f2
...
b1c59c45eb
Author | SHA1 | Date | |
---|---|---|---|
|
b1c59c45eb | ||
|
a218463c73 | ||
|
af261006a3 | ||
|
b2cad74b03 | ||
|
1700d2f4f2 | ||
|
e26a74ab1d | ||
|
133a6b3d4a | ||
|
a5101870a5 |
@ -1,7 +1,7 @@
|
||||
version: '3'
|
||||
services:
|
||||
httpx:
|
||||
image: ruby:3.1
|
||||
image: ruby:3.2
|
||||
environment:
|
||||
- HTTPBIN_COALESCING_HOST=another
|
||||
- HTTPX_RESOLVER_URI=https://doh/dns-query
|
||||
|
@ -45,10 +45,12 @@ module HTTPX
|
||||
|
||||
def_delegator :@write_buffer, :empty?
|
||||
|
||||
attr_reader :io, :origin, :origins, :state, :pending, :options
|
||||
attr_reader :type, :io, :origin, :origins, :state, :pending, :options
|
||||
|
||||
attr_writer :timers
|
||||
|
||||
attr_accessor :family
|
||||
|
||||
def initialize(type, uri, options)
|
||||
@type = type
|
||||
@origins = [uri.origin]
|
||||
@ -76,13 +78,6 @@ 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)
|
||||
@ -121,7 +116,10 @@ module HTTPX
|
||||
|
||||
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
|
||||
|
||||
# coalescable connections need to be mergeable!
|
||||
@ -226,6 +224,14 @@ 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)
|
||||
@ -527,11 +533,12 @@ module HTTPX
|
||||
Errno::EINVAL,
|
||||
Errno::ENETUNREACH,
|
||||
Errno::EPIPE,
|
||||
Errno::ENOENT => e
|
||||
Errno::ENOENT,
|
||||
SocketError => e
|
||||
# connect errors, exit gracefully
|
||||
error = ConnectionError.new(e.message)
|
||||
error.set_backtrace(e.backtrace)
|
||||
handle_error(error)
|
||||
connecting? && callbacks(:connect_error).any? ? emit(:connect_error, error) : handle_error(error)
|
||||
@state = :closed
|
||||
emit(:close)
|
||||
rescue TLSError => e
|
||||
@ -550,6 +557,8 @@ module HTTPX
|
||||
return if @state == :closed
|
||||
|
||||
@io.connect
|
||||
emit(:tcp_open) if @io.state == :connected
|
||||
|
||||
return unless @io.connected?
|
||||
|
||||
@connected_at = Utils.now
|
||||
|
@ -160,6 +160,8 @@ 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
|
||||
|
@ -76,11 +76,7 @@ module HTTPX
|
||||
Errno::EADDRNOTAVAIL,
|
||||
Errno::EHOSTUNREACH,
|
||||
SocketError => e
|
||||
if @ip_index <= 0
|
||||
error = ConnectionError.new(e.message)
|
||||
error.set_backtrace(e.backtrace)
|
||||
raise error
|
||||
end
|
||||
raise e if @ip_index <= 0
|
||||
|
||||
log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
|
||||
@ip_index -= 1
|
||||
|
@ -72,7 +72,7 @@ module HTTPX
|
||||
end
|
||||
|
||||
def init_connection(connection, _options)
|
||||
resolve_connection(connection)
|
||||
resolve_connection(connection) unless connection.family
|
||||
connection.timers = @timers
|
||||
connection.on(:open) do
|
||||
@connected_connections += 1
|
||||
@ -116,18 +116,55 @@ 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 << connection
|
||||
resolver << try_clone_connection(connection, resolver.family)
|
||||
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|
|
||||
|
@ -108,7 +108,15 @@ 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
|
||||
|
@ -136,9 +136,22 @@ module HTTPX
|
||||
emit_resolve_error(connection, connection.origin.host, e)
|
||||
return
|
||||
end
|
||||
if answers.nil? || answers.empty?
|
||||
|
||||
if answers.nil?
|
||||
# Indicates no such domain was found.
|
||||
|
||||
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
|
||||
|
||||
|
@ -215,7 +215,8 @@ module HTTPX
|
||||
raise ex
|
||||
end
|
||||
|
||||
if addresses.nil? || addresses.empty?
|
||||
if addresses.nil?
|
||||
# Indicates no such domain was found.
|
||||
hostname, connection = @queries.first
|
||||
@queries.delete(hostname)
|
||||
@timeouts.delete(hostname)
|
||||
@ -224,6 +225,14 @@ 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"]
|
||||
@ -310,7 +319,9 @@ module HTTPX
|
||||
ip, port = @nameserver[@ns_index]
|
||||
port ||= DNS_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)
|
||||
log { "resolver: server: #{uri}..." }
|
||||
@io = type.new(uri, [IPAddr.new(ip)], @options)
|
||||
|
@ -73,11 +73,6 @@ 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)
|
||||
|
@ -21,6 +21,7 @@ module HTTPX
|
||||
|
||||
BUFFER_SIZE: Integer
|
||||
|
||||
attr_reader type: io_type
|
||||
attr_reader origin: URI::Generic
|
||||
attr_reader origins: Array[String]
|
||||
attr_reader state: Symbol
|
||||
@ -28,7 +29,8 @@ module HTTPX
|
||||
attr_reader options: Options
|
||||
attr_writer timers: Timers
|
||||
|
||||
@type: io_type
|
||||
attr_accessor family: Integer?
|
||||
|
||||
@window_size: Integer
|
||||
@read_buffer: Buffer
|
||||
@write_buffer: Buffer
|
||||
@ -36,8 +38,6 @@ module HTTPX
|
||||
@keep_alive_timeout: Numeric?
|
||||
@total_timeout: Numeric?
|
||||
|
||||
def clone_new_connection: () -> instance
|
||||
|
||||
def addresses: () -> Array[ipaddr]?
|
||||
|
||||
def addresses=: (Array[ipaddr]) -> void
|
||||
|
@ -22,7 +22,9 @@ module HTTPX
|
||||
|
||||
private
|
||||
|
||||
def initialize: () -> untyped
|
||||
def initialize: () -> void
|
||||
|
||||
def try_clone_connection: (Connection connection, Integer? family) -> Connection
|
||||
|
||||
def resolve_connection: (Connection) -> void
|
||||
|
||||
|
@ -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
|
@ -6,7 +6,8 @@ module HTTPX
|
||||
DEFAULTS: Hash[Symbol, untyped]
|
||||
FAMILY_TYPES: Hash[singleton(Resolv::DNS::Resource), String]
|
||||
|
||||
@family: ip_family
|
||||
attr_reader family: ip_family
|
||||
|
||||
@options: Options
|
||||
@requests: Hash[Request, String]
|
||||
@connections: Array[Connection]
|
||||
@ -33,7 +34,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
|
@ -7,7 +7,8 @@ module HTTPX
|
||||
DEFAULTS: Hash[Symbol, untyped]
|
||||
DNS_PORT: Integer
|
||||
|
||||
@family: ip_family
|
||||
attr_reader family: ip_family
|
||||
|
||||
@options: Options
|
||||
@ns_index: Integer
|
||||
@nameserver: Array[String]?
|
||||
|
@ -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,6 +24,8 @@ 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
|
||||
|
@ -23,7 +23,7 @@ module Requests
|
||||
return unless uri.start_with?("http://")
|
||||
|
||||
response = HTTPX.get(uri, addresses: [EHOSTUNREACH_HOST] * 2)
|
||||
verify_error_response(response, Errno::EHOSTUNREACH)
|
||||
verify_error_response(response, /No route to host/)
|
||||
end
|
||||
|
||||
# TODO: reset this test once it's possible to test ETIMEDOUT again
|
||||
|
Loading…
x
Reference in New Issue
Block a user