httpx 0.18.5 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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