httpx 0.18.5 → 0.19.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -1
  3. data/doc/release_notes/0_18_5.md +2 -2
  4. data/doc/release_notes/0_18_6.md +5 -0
  5. data/doc/release_notes/0_18_7.md +5 -0
  6. data/doc/release_notes/0_19_0.md +39 -0
  7. data/doc/release_notes/0_19_1.md +5 -0
  8. data/lib/httpx/adapters/faraday.rb +7 -3
  9. data/lib/httpx/connection/http1.rb +5 -5
  10. data/lib/httpx/connection/http2.rb +1 -5
  11. data/lib/httpx/connection.rb +22 -10
  12. data/lib/httpx/extensions.rb +16 -0
  13. data/lib/httpx/headers.rb +0 -2
  14. data/lib/httpx/io/tcp.rb +27 -6
  15. data/lib/httpx/options.rb +44 -11
  16. data/lib/httpx/plugins/cookies.rb +5 -7
  17. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  18. data/lib/httpx/plugins/multipart/mime_type_detector.rb +7 -1
  19. data/lib/httpx/plugins/proxy/http.rb +10 -23
  20. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  21. data/lib/httpx/plugins/proxy/socks5.rb +1 -1
  22. data/lib/httpx/plugins/proxy.rb +20 -12
  23. data/lib/httpx/plugins/retries.rb +1 -1
  24. data/lib/httpx/pool.rb +40 -20
  25. data/lib/httpx/resolver/https.rb +32 -42
  26. data/lib/httpx/resolver/multi.rb +79 -0
  27. data/lib/httpx/resolver/native.rb +28 -36
  28. data/lib/httpx/resolver/resolver.rb +92 -0
  29. data/lib/httpx/resolver/system.rb +175 -19
  30. data/lib/httpx/resolver.rb +37 -11
  31. data/lib/httpx/response.rb +4 -2
  32. data/lib/httpx/session.rb +1 -15
  33. data/lib/httpx/session_extensions.rb +26 -0
  34. data/lib/httpx/timers.rb +1 -1
  35. data/lib/httpx/transcoder/chunker.rb +0 -1
  36. data/lib/httpx/version.rb +1 -1
  37. data/lib/httpx.rb +3 -0
  38. data/sig/connection/http1.rbs +0 -2
  39. data/sig/connection/http2.rbs +2 -2
  40. data/sig/connection.rbs +1 -0
  41. data/sig/errors.rbs +8 -0
  42. data/sig/headers.rbs +0 -2
  43. data/sig/httpx.rbs +4 -0
  44. data/sig/options.rbs +10 -7
  45. data/sig/parser/http1.rbs +14 -5
  46. data/sig/pool.rbs +17 -9
  47. data/sig/registry.rbs +3 -0
  48. data/sig/request.rbs +11 -0
  49. data/sig/resolver/https.rbs +15 -27
  50. data/sig/resolver/multi.rbs +7 -0
  51. data/sig/resolver/native.rbs +3 -12
  52. data/sig/resolver/resolver.rbs +36 -0
  53. data/sig/resolver/system.rbs +3 -9
  54. data/sig/resolver.rbs +12 -10
  55. data/sig/response.rbs +15 -5
  56. data/sig/selector.rbs +3 -3
  57. data/sig/timers.rbs +5 -2
  58. data/sig/transcoder/chunker.rbs +16 -5
  59. data/sig/transcoder/json.rbs +5 -0
  60. data/sig/transcoder.rbs +3 -1
  61. metadata +15 -4
  62. data/lib/httpx/resolver/resolver_mixin.rb +0 -75
  63. data/sig/resolver/resolver_mixin.rbs +0 -26
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "resolv"
4
- require "ipaddr"
5
- require "forwardable"
6
-
7
3
  module HTTPX
8
4
  class HTTPProxyError < Error; end
9
5
 
@@ -85,7 +81,7 @@ module HTTPX
85
81
  end
86
82
  uris
87
83
  end
88
- options.proxy.merge(uri: @_proxy_uris.first) unless @_proxy_uris.empty?
84
+ { uri: @_proxy_uris.first } unless @_proxy_uris.empty?
89
85
  end
90
86
 
91
87
  def find_connection(request, connections, options)
@@ -109,12 +105,15 @@ module HTTPX
109
105
  return super unless proxy
110
106
 
111
107
  connection = options.connection_class.new("tcp", uri, options)
112
- pool.init_connection(connection, options)
113
- connection
108
+ catch(:coalesced) do
109
+ pool.init_connection(connection, options)
110
+ connection
111
+ end
114
112
  end
115
113
 
116
114
  def fetch_response(request, connections, options)
117
115
  response = super
116
+
118
117
  if response.is_a?(ErrorResponse) &&
119
118
  __proxy_error?(response) && !@_proxy_uris.empty?
120
119
  @_proxy_uris.shift
@@ -181,11 +180,20 @@ module HTTPX
181
180
  super && @options.proxy == options.proxy
182
181
  end
183
182
 
184
- # should not coalesce connections here, as the IP is the IP of the proxy
185
- def coalescable?(*)
183
+ def coalescable?(connection)
186
184
  return super unless @options.proxy
187
185
 
188
- false
186
+ if @io.protocol == "h2" &&
187
+ @origin.scheme == "https" &&
188
+ connection.origin.scheme == "https" &&
189
+ @io.can_verify_peer?
190
+ # in proxied connections, .origin is the proxy ; Given names
191
+ # are stored in .origins, this is what is used.
192
+ origin = URI(connection.origins.first)
193
+ @io.verify_hostname(origin.host)
194
+ else
195
+ @origin == connection.origin
196
+ end
189
197
  end
190
198
 
191
199
  def send(request)
@@ -234,13 +242,13 @@ module HTTPX
234
242
  end
235
243
  end
236
244
 
237
- def transition(nextstate)
245
+ def handle_transition(nextstate)
238
246
  return super unless @options.proxy
239
247
 
240
248
  case nextstate
241
249
  when :closing
242
250
  # this is a hack so that we can use the super method
243
- # and it'll thing that the current state is open
251
+ # and it'll think that the current state is open
244
252
  @state = :open if @state == :connecting
245
253
  end
246
254
  super
@@ -96,8 +96,8 @@ module HTTPX
96
96
  # rubocop:enable Style/MultilineTernaryOperator
97
97
  )
98
98
  response.close if response.respond_to?(:close)
99
- request.retries -= 1
100
99
  log { "failed to get response, #{request.retries} tries to go..." }
100
+ request.retries -= 1
101
101
  request.transition(:idle)
102
102
 
103
103
  retry_after = options.retry_after
data/lib/httpx/pool.rb CHANGED
@@ -14,7 +14,6 @@ module HTTPX
14
14
 
15
15
  def initialize
16
16
  @resolvers = {}
17
- @_resolver_ios = {}
18
17
  @timers = Timers.new
19
18
  @selector = Selector.new
20
19
  @connections = []
@@ -56,9 +55,20 @@ module HTTPX
56
55
  connections = connections.reject(&:inflight?)
57
56
  connections.each(&:close)
58
57
  next_tick until connections.none? { |c| c.state != :idle && @connections.include?(c) }
58
+
59
+ # close resolvers
60
+ outstanding_connections = @connections
61
+ resolver_connections = @resolvers.each_value.flat_map(&:connections).compact
62
+ outstanding_connections -= resolver_connections
63
+
64
+ return unless outstanding_connections.empty?
65
+
59
66
  @resolvers.each_value do |resolver|
60
67
  resolver.close unless resolver.closed?
61
- end if @connections.empty?
68
+ end
69
+ # for https resolver
70
+ resolver_connections.each(&:close)
71
+ next_tick until resolver_connections.none? { |c| c.state != :idle && @connections.include?(c) }
62
72
  end
63
73
 
64
74
  def init_connection(connection, _options)
@@ -107,11 +117,12 @@ module HTTPX
107
117
  return
108
118
  end
109
119
 
110
- resolver = find_resolver_for(connection)
111
- resolver << connection
112
- return if resolver.empty?
120
+ find_resolver_for(connection) do |resolver|
121
+ resolver << connection
122
+ next if resolver.empty?
113
123
 
114
- @_resolver_ios[resolver] ||= select_connection(resolver)
124
+ select_connection(resolver)
125
+ end
115
126
  end
116
127
 
117
128
  def on_resolver_connection(connection)
@@ -138,12 +149,11 @@ module HTTPX
138
149
 
139
150
  def on_resolver_close(resolver)
140
151
  resolver_type = resolver.class
141
- return unless @resolvers[resolver_type] == resolver
152
+ return if resolver.closed?
142
153
 
143
154
  @resolvers.delete(resolver_type)
144
155
 
145
156
  deselect_connection(resolver)
146
- @_resolver_ios.delete(resolver)
147
157
  resolver.close unless resolver.closed?
148
158
  end
149
159
 
@@ -174,12 +184,10 @@ module HTTPX
174
184
  end
175
185
 
176
186
  def coalesce_connections(conn1, conn2)
177
- if conn1.coalescable?(conn2)
178
- conn1.merge(conn2)
179
- @connections.delete(conn2)
180
- else
181
- register_connection(conn2)
182
- end
187
+ return register_connection(conn2) unless conn1.coalescable?(conn2)
188
+
189
+ conn1.merge(conn2)
190
+ @connections.delete(conn2)
183
191
  end
184
192
 
185
193
  def next_timeout
@@ -196,13 +204,25 @@ module HTTPX
196
204
  resolver_type = Resolver.registry(resolver_type) if resolver_type.is_a?(Symbol)
197
205
 
198
206
  @resolvers[resolver_type] ||= begin
199
- resolver = resolver_type.new(connection_options)
200
- resolver.pool = self if resolver.respond_to?(:pool=)
201
- resolver.on(:resolve, &method(:on_resolver_connection))
202
- resolver.on(:error, &method(:on_resolver_error))
203
- resolver.on(:close) { on_resolver_close(resolver) }
204
- resolver
207
+ resolver_manager = if resolver_type.multi?
208
+ Resolver::Multi.new(resolver_type, connection_options)
209
+ else
210
+ resolver_type.new(connection_options)
211
+ end
212
+ resolver_manager.on(:resolve, &method(:on_resolver_connection))
213
+ resolver_manager.on(:error, &method(:on_resolver_error))
214
+ resolver_manager.on(:close, &method(:on_resolver_close))
215
+ resolver_manager
216
+ end
217
+
218
+ manager = @resolvers[resolver_type]
219
+
220
+ (manager.is_a?(Resolver::Multi) && manager.early_resolve(connection)) || manager.resolvers.each do |resolver|
221
+ resolver.pool = self
222
+ yield resolver
205
223
  end
224
+
225
+ manager
206
226
  end
207
227
  end
208
228
  end
@@ -6,32 +6,23 @@ require "cgi"
6
6
  require "forwardable"
7
7
 
8
8
  module HTTPX
9
- class Resolver::HTTPS
9
+ class Resolver::HTTPS < Resolver::Resolver
10
10
  extend Forwardable
11
- include Resolver::ResolverMixin
12
11
  using URIExtensions
12
+ using StringExtensions
13
13
 
14
14
  NAMESERVER = "https://1.1.1.1/dns-query"
15
15
 
16
- RECORD_TYPES = {
17
- "A" => Resolv::DNS::Resource::IN::A,
18
- "AAAA" => Resolv::DNS::Resource::IN::AAAA,
19
- }.freeze
20
-
21
16
  DEFAULTS = {
22
17
  uri: NAMESERVER,
23
18
  use_get: false,
24
- record_types: RECORD_TYPES.keys,
25
19
  }.freeze
26
20
 
27
21
  def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close
28
22
 
29
- attr_writer :pool
30
-
31
- def initialize(options)
32
- @options = Options.new(options)
23
+ def initialize(_, options)
24
+ super
33
25
  @resolver_options = DEFAULTS.merge(@options.resolver_options)
34
- @_record_types = Hash.new { |types, host| types[host] = @resolver_options[:record_types].dup }
35
26
  @queries = {}
36
27
  @requests = {}
37
28
  @connections = []
@@ -44,7 +35,7 @@ module HTTPX
44
35
  def <<(connection)
45
36
  return if @uri.origin == connection.origin.to_s
46
37
 
47
- @uri_addresses ||= ip_resolve(@uri.host) || system_resolve(@uri.host) || @resolver.getaddresses(@uri.host)
38
+ @uri_addresses ||= HTTPX::Resolver.nolookup_resolve(@uri.host) || @resolver.getaddresses(@uri.host)
48
39
 
49
40
  if @uri_addresses.empty?
50
41
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
@@ -52,7 +43,7 @@ module HTTPX
52
43
  throw(:resolve_error, ex)
53
44
  end
54
45
 
55
- early_resolve(connection) || resolve(connection)
46
+ resolve(connection)
56
47
  end
57
48
 
58
49
  def closed?
@@ -63,21 +54,22 @@ module HTTPX
63
54
  true
64
55
  end
65
56
 
66
- private
67
-
68
57
  def resolver_connection
69
58
  @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
70
59
  @building_connection = true
71
60
  connection = @options.connection_class.new("ssl", @uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
72
61
  @pool.init_connection(connection, @options)
73
- emit_addresses(connection, @uri_addresses)
62
+ emit_addresses(connection, @family, @uri_addresses)
74
63
  @building_connection = false
75
64
  connection
76
65
  end
77
66
  end
78
67
 
68
+ private
69
+
79
70
  def resolve(connection = @connections.first, hostname = nil)
80
71
  return if @building_connection
72
+ return unless connection
81
73
 
82
74
  hostname ||= @queries.key(connection)
83
75
 
@@ -85,17 +77,16 @@ module HTTPX
85
77
  hostname = connection.origin.host
86
78
  log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
87
79
  end
88
- type = @_record_types[hostname].first || "A"
89
- log { "resolver: query #{type} for #{hostname}" }
80
+ log { "resolver: query #{FAMILY_TYPES[RECORD_TYPES[@family]]} for #{hostname}" }
90
81
  begin
91
- request = build_request(hostname, type)
82
+ request = build_request(hostname)
92
83
  request.on(:response, &method(:on_response).curry(2)[request])
93
84
  request.on(:promise, &method(:on_promise))
94
85
  @requests[request] = connection
95
86
  resolver_connection.send(request)
96
87
  @queries[hostname] = connection
97
88
  @connections << connection
98
- rescue Resolv::DNS::EncodeError, JSON::JSONError => e
89
+ rescue ResolveError, Resolv::DNS::EncodeError, JSON::JSONError => e
99
90
  emit_resolve_error(connection, hostname, e)
100
91
  end
101
92
  end
@@ -107,6 +98,7 @@ module HTTPX
107
98
  hostname = @queries.key(connection)
108
99
  emit_resolve_error(connection, hostname, e)
109
100
  else
101
+ # @type var response: HTTPX::Response
110
102
  parse(response)
111
103
  ensure
112
104
  @requests.delete(request)
@@ -122,21 +114,15 @@ module HTTPX
122
114
  answers = decode_response_body(response)
123
115
  rescue Resolv::DNS::DecodeError, JSON::JSONError => e
124
116
  host, connection = @queries.first
125
- if @_record_types[host].empty?
126
- @queries.delete(host)
127
- emit_resolve_error(connection, host, e)
128
- return
129
- end
117
+ @queries.delete(host)
118
+ emit_resolve_error(connection, host, e)
119
+ return
130
120
  end
131
121
  if answers.nil? || answers.empty?
132
122
  host, connection = @queries.first
133
- @_record_types[host].shift
134
- if @_record_types[host].empty?
135
- @queries.delete(host)
136
- @_record_types.delete(host)
137
- emit_resolve_error(connection, host)
138
- return
139
- end
123
+ @queries.delete(host)
124
+ emit_resolve_error(connection, host)
125
+ return
140
126
  else
141
127
  answers = answers.group_by { |answer| answer["name"] }
142
128
  answers.each do |hostname, addresses|
@@ -146,8 +132,12 @@ module HTTPX
146
132
  if alias_address.nil?
147
133
  connection = @queries[hostname]
148
134
  @queries.delete(address["name"])
149
- resolve(connection, address["alias"])
150
- return # rubocop:disable Lint/NonLocalExitFromIterator
135
+ if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
136
+ @connections.delete(connection)
137
+ else
138
+ resolve(connection, address["alias"])
139
+ return # rubocop:disable Lint/NonLocalExitFromIterator
140
+ end
151
141
  else
152
142
  alias_address
153
143
  end
@@ -157,13 +147,13 @@ module HTTPX
157
147
  end.compact
158
148
  next if addresses.empty?
159
149
 
160
- hostname = hostname[0..-2] if hostname.end_with?(".")
150
+ hostname.delete_suffix!(".") if hostname.end_with?(".")
161
151
  connection = @queries.delete(hostname)
162
152
  next unless connection # probably a retried query for which there's an answer
163
153
 
164
154
  @connections.delete(connection)
165
- Resolver.cached_lookup_set(hostname, addresses) if @resolver_options[:cache]
166
- emit_addresses(connection, addresses.map { |addr| addr["data"] })
155
+ Resolver.cached_lookup_set(hostname, @family, addresses) if @resolver_options[:cache]
156
+ emit_addresses(connection, @family, addresses.map { |addr| addr["data"] })
167
157
  end
168
158
  end
169
159
  return if @connections.empty?
@@ -171,14 +161,14 @@ module HTTPX
171
161
  resolve
172
162
  end
173
163
 
174
- def build_request(hostname, type)
164
+ def build_request(hostname)
175
165
  uri = @uri.dup
176
166
  rklass = @options.request_class
177
- payload = Resolver.encode_dns_query(hostname, type: RECORD_TYPES[type])
167
+ payload = Resolver.encode_dns_query(hostname, type: @record_type)
178
168
 
179
169
  if @resolver_options[:use_get]
180
170
  params = URI.decode_www_form(uri.query.to_s)
181
- params << ["type", type]
171
+ params << ["type", FAMILY_TYPES[@record_type]]
182
172
  params << ["dns", Base64.urlsafe_encode64(payload, padding: false)]
183
173
  uri.query = URI.encode_www_form(params)
184
174
  request = rklass.new("GET", uri, @options)
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "resolv"
5
+
6
+ module HTTPX
7
+ class Resolver::Multi
8
+ include Callbacks
9
+ using ArrayExtensions
10
+
11
+ attr_reader :resolvers
12
+
13
+ def initialize(resolver_type, options)
14
+ @options = options
15
+ @resolver_options = @options.resolver_options
16
+
17
+ @resolvers = options.ip_families.map do |ip_family|
18
+ resolver = resolver_type.new(ip_family, options)
19
+ resolver.on(:resolve, &method(:on_resolver_connection))
20
+ resolver.on(:error, &method(:on_resolver_error))
21
+ resolver.on(:close) { on_resolver_close(resolver) }
22
+ resolver
23
+ end
24
+
25
+ @errors = Hash.new { |hs, k| hs[k] = [] }
26
+ end
27
+
28
+ def closed?
29
+ @resolvers.all?(&:closed?)
30
+ end
31
+
32
+ def timeout
33
+ @resolvers.filter_map(&:timeout).min
34
+ end
35
+
36
+ def close
37
+ @resolvers.each(&:close)
38
+ end
39
+
40
+ def connections
41
+ @resolvers.filter_map { |r| r.resolver_connection if r.respond_to?(:resolver_connection) }
42
+ end
43
+
44
+ def early_resolve(connection)
45
+ hostname = connection.origin.host
46
+ addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
47
+ return unless addresses
48
+
49
+ addresses = addresses.group_by(&:family)
50
+
51
+ @resolvers.each do |resolver|
52
+ addrs = addresses[resolver.family]
53
+
54
+ next if !addrs || addrs.empty?
55
+
56
+ resolver.emit_addresses(connection, resolver.family, addrs)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def on_resolver_connection(connection)
63
+ emit(:resolve, connection)
64
+ end
65
+
66
+ def on_resolver_error(connection, error)
67
+ @errors[connection] << error
68
+
69
+ return unless @errors[connection].size >= @resolvers.size
70
+
71
+ errors = @errors.delete(connection)
72
+ emit(:error, connection, errors.first)
73
+ end
74
+
75
+ def on_resolver_close(resolver)
76
+ emit(:close, resolver)
77
+ end
78
+ end
79
+ end
@@ -4,22 +4,15 @@ require "forwardable"
4
4
  require "resolv"
5
5
 
6
6
  module HTTPX
7
- class Resolver::Native
7
+ class Resolver::Native < Resolver::Resolver
8
8
  extend Forwardable
9
- include Resolver::ResolverMixin
10
9
  using URIExtensions
11
10
 
12
- RECORD_TYPES = {
13
- "A" => Resolv::DNS::Resource::IN::A,
14
- "AAAA" => Resolv::DNS::Resource::IN::AAAA,
15
- }.freeze
16
-
17
11
  DEFAULTS = if RUBY_VERSION < "2.2"
18
12
  {
19
13
  **Resolv::DNS::Config.default_config_hash,
20
14
  packet_size: 512,
21
15
  timeouts: Resolver::RESOLVE_TIMEOUT,
22
- record_types: RECORD_TYPES.keys,
23
16
  }.freeze
24
17
  else
25
18
  {
@@ -27,7 +20,6 @@ module HTTPX
27
20
  **Resolv::DNS::Config.default_config_hash,
28
21
  packet_size: 512,
29
22
  timeouts: Resolver::RESOLVE_TIMEOUT,
30
- record_types: RECORD_TYPES.keys,
31
23
  }.freeze
32
24
  end
33
25
 
@@ -49,14 +41,13 @@ module HTTPX
49
41
 
50
42
  attr_reader :state
51
43
 
52
- def initialize(options)
53
- @options = Options.new(options)
44
+ def initialize(_, options)
45
+ super
54
46
  @ns_index = 0
55
47
  @resolver_options = DEFAULTS.merge(@options.resolver_options)
56
48
  @nameserver = @resolver_options[:nameserver]
57
49
  @_timeouts = Array(@resolver_options[:timeouts])
58
50
  @timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
59
- @_record_types = Hash.new { |types, host| types[host] = @resolver_options[:record_types].dup }
60
51
  @connections = []
61
52
  @queries = {}
62
53
  @read_buffer = "".b
@@ -107,8 +98,6 @@ module HTTPX
107
98
  end
108
99
 
109
100
  def <<(connection)
110
- return if early_resolve(connection)
111
-
112
101
  if @nameserver.nil?
113
102
  ex = ResolveError.new("No available nameserver")
114
103
  ex.set_backtrace(caller)
@@ -140,7 +129,7 @@ module HTTPX
140
129
  end
141
130
 
142
131
  def do_retry
143
- return if @queries.empty?
132
+ return if @queries.empty? || !@start_timeout
144
133
 
145
134
  loop_time = Utils.elapsed_time(@start_timeout)
146
135
  connections = []
@@ -160,7 +149,7 @@ module HTTPX
160
149
  @connections.delete(connection)
161
150
  # This loop_time passed to the exception is bogus. Ideally we would pass the total
162
151
  # resolve timeout, including from the previous retries.
163
- raise ResolveTimeoutError.new(loop_time, "Timed out")
152
+ raise ResolveTimeoutError.new(loop_time, "Timed out while resolving #{host}")
164
153
  # raise NativeResolveError.new(connection, host)
165
154
  else
166
155
  log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
@@ -198,25 +187,21 @@ module HTTPX
198
187
  addresses = Resolver.decode_dns_answer(buffer)
199
188
  rescue Resolv::DNS::DecodeError => e
200
189
  hostname, connection = @queries.first
201
- if @_record_types[hostname].empty?
202
- @queries.delete(hostname)
203
- @connections.delete(connection)
204
- ex = NativeResolveError.new(connection, hostname, e.message)
205
- ex.set_backtrace(e.backtrace)
206
- raise ex
207
- end
190
+ @queries.delete(hostname)
191
+ @timeouts.delete(hostname)
192
+ @connections.delete(connection)
193
+ ex = NativeResolveError.new(connection, hostname, e.message)
194
+ ex.set_backtrace(e.backtrace)
195
+ raise ex
208
196
  end
209
197
 
210
198
  if addresses.nil? || addresses.empty?
211
199
  hostname, connection = @queries.first
212
- @_record_types[hostname].shift
213
- if @_record_types[hostname].empty?
214
- @queries.delete(hostname)
215
- @_record_types.delete(hostname)
216
- @connections.delete(connection)
200
+ @queries.delete(hostname)
201
+ @timeouts.delete(hostname)
202
+ @connections.delete(connection)
217
203
 
218
- raise NativeResolveError.new(connection, hostname)
219
- end
204
+ raise NativeResolveError.new(connection, hostname)
220
205
  else
221
206
  address = addresses.first
222
207
  name = address["name"]
@@ -236,16 +221,21 @@ module HTTPX
236
221
  end
237
222
 
238
223
  if address.key?("alias") # CNAME
239
- if early_resolve(connection, hostname: address["alias"])
224
+ # clean up intermediate queries
225
+ @timeouts.delete(name) unless connection.origin.host == name
226
+
227
+ if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
240
228
  @connections.delete(connection)
241
229
  else
242
230
  resolve(connection, address["alias"])
243
231
  return
244
232
  end
245
233
  else
234
+ @timeouts.delete(name)
235
+ @timeouts.delete(connection.origin.host)
246
236
  @connections.delete(connection)
247
- Resolver.cached_lookup_set(connection.origin.host, addresses) if @resolver_options[:cache]
248
- emit_addresses(connection, addresses.map { |addr| addr["data"] })
237
+ Resolver.cached_lookup_set(connection.origin.host, @family, addresses) if @resolver_options[:cache]
238
+ emit_addresses(connection, @family, addresses.map { |addr| addr["data"] })
249
239
  end
250
240
  end
251
241
  return emit(:close) if @connections.empty?
@@ -264,10 +254,9 @@ module HTTPX
264
254
  log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
265
255
  end
266
256
  @queries[hostname] = connection
267
- type = @_record_types[hostname].first || "A"
268
- log { "resolver: query #{type} for #{hostname}" }
257
+ log { "resolver: query #{@record_type.name.split("::").last} for #{hostname}" }
269
258
  begin
270
- @write_buffer << Resolver.encode_dns_query(hostname, type: RECORD_TYPES[type])
259
+ @write_buffer << Resolver.encode_dns_query(hostname, type: @record_type)
271
260
  rescue Resolv::DNS::EncodeError => e
272
261
  emit_resolve_error(connection, hostname, e)
273
262
  end
@@ -306,6 +295,9 @@ module HTTPX
306
295
  return unless @state == :open
307
296
 
308
297
  @io.close if @io
298
+ @start_timeout = nil
299
+ @write_buffer.clear
300
+ @read_buffer.clear
309
301
  end
310
302
  @state = nextstate
311
303
  end