mirror of
https://github.com/HoneyryderChuck/httpx.git
synced 2025-07-15 00:00:55 -04:00
Compare commits
No commits in common. "b1c59c45ebe3dd24b8192d85a08288e96ab04ced" and "84f5b303f268f721d0f98d2864544715dc4abc67" have entirely different histories.
b1c59c45eb
...
84f5b303f2
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
@ -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
|
@ -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]?
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user