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'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
httpx:
|
httpx:
|
||||||
image: ruby:3.1
|
image: ruby:3.2
|
||||||
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,10 +45,12 @@ module HTTPX
|
|||||||
|
|
||||||
def_delegator :@write_buffer, :empty?
|
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_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]
|
||||||
@ -76,13 +78,6 @@ 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)
|
||||||
@ -121,7 +116,10 @@ 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!
|
||||||
@ -226,6 +224,14 @@ 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)
|
||||||
@ -527,11 +533,12 @@ module HTTPX
|
|||||||
Errno::EINVAL,
|
Errno::EINVAL,
|
||||||
Errno::ENETUNREACH,
|
Errno::ENETUNREACH,
|
||||||
Errno::EPIPE,
|
Errno::EPIPE,
|
||||||
Errno::ENOENT => e
|
Errno::ENOENT,
|
||||||
|
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)
|
||||||
handle_error(error)
|
connecting? && callbacks(:connect_error).any? ? emit(:connect_error, error) : handle_error(error)
|
||||||
@state = :closed
|
@state = :closed
|
||||||
emit(:close)
|
emit(:close)
|
||||||
rescue TLSError => e
|
rescue TLSError => e
|
||||||
@ -550,6 +557,8 @@ 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,6 +160,8 @@ 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,11 +76,7 @@ module HTTPX
|
|||||||
Errno::EADDRNOTAVAIL,
|
Errno::EADDRNOTAVAIL,
|
||||||
Errno::EHOSTUNREACH,
|
Errno::EHOSTUNREACH,
|
||||||
SocketError => e
|
SocketError => e
|
||||||
if @ip_index <= 0
|
raise e 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)
|
resolve_connection(connection) unless connection.family
|
||||||
connection.timers = @timers
|
connection.timers = @timers
|
||||||
connection.on(:open) do
|
connection.on(:open) do
|
||||||
@connected_connections += 1
|
@connected_connections += 1
|
||||||
@ -116,18 +116,55 @@ 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 << connection
|
resolver << try_clone_connection(connection, resolver.family)
|
||||||
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,7 +108,15 @@ 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,9 +136,22 @@ 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,7 +215,8 @@ module HTTPX
|
|||||||
raise ex
|
raise ex
|
||||||
end
|
end
|
||||||
|
|
||||||
if addresses.nil? || addresses.empty?
|
if addresses.nil?
|
||||||
|
# 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)
|
||||||
@ -224,6 +225,14 @@ 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"]
|
||||||
@ -310,7 +319,9 @@ 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,11 +73,6 @@ 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,6 +21,7 @@ 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
|
||||||
@ -28,7 +29,8 @@ module HTTPX
|
|||||||
attr_reader options: Options
|
attr_reader options: Options
|
||||||
attr_writer timers: Timers
|
attr_writer timers: Timers
|
||||||
|
|
||||||
@type: io_type
|
attr_accessor family: Integer?
|
||||||
|
|
||||||
@window_size: Integer
|
@window_size: Integer
|
||||||
@read_buffer: Buffer
|
@read_buffer: Buffer
|
||||||
@write_buffer: Buffer
|
@write_buffer: Buffer
|
||||||
@ -36,8 +38,6 @@ 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,7 +22,9 @@ module HTTPX
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def initialize: () -> untyped
|
def initialize: () -> void
|
||||||
|
|
||||||
|
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,7 +6,8 @@ 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]
|
||||||
|
|
||||||
@family: ip_family
|
attr_reader family: ip_family
|
||||||
|
|
||||||
@options: Options
|
@options: Options
|
||||||
@requests: Hash[Request, String]
|
@requests: Hash[Request, String]
|
||||||
@connections: Array[Connection]
|
@connections: Array[Connection]
|
||||||
@ -33,7 +34,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,7 +7,8 @@ module HTTPX
|
|||||||
DEFAULTS: Hash[Symbol, untyped]
|
DEFAULTS: Hash[Symbol, untyped]
|
||||||
DNS_PORT: Integer
|
DNS_PORT: Integer
|
||||||
|
|
||||||
@family: ip_family
|
attr_reader 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,6 +24,8 @@ 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, Errno::EHOSTUNREACH)
|
verify_error_response(response, /No route to host/)
|
||||||
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