httpx 1.3.4 → 1.4.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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_4_0.md +43 -0
  3. data/doc/release_notes/1_4_1.md +19 -0
  4. data/lib/httpx/adapters/datadog.rb +55 -83
  5. data/lib/httpx/adapters/faraday.rb +2 -0
  6. data/lib/httpx/adapters/webmock.rb +18 -6
  7. data/lib/httpx/callbacks.rb +0 -5
  8. data/lib/httpx/chainable.rb +3 -1
  9. data/lib/httpx/connection/http2.rb +12 -8
  10. data/lib/httpx/connection.rb +192 -22
  11. data/lib/httpx/errors.rb +12 -0
  12. data/lib/httpx/loggable.rb +5 -5
  13. data/lib/httpx/options.rb +26 -16
  14. data/lib/httpx/plugins/aws_sigv4.rb +31 -16
  15. data/lib/httpx/plugins/callbacks.rb +12 -2
  16. data/lib/httpx/plugins/circuit_breaker.rb +0 -5
  17. data/lib/httpx/plugins/content_digest.rb +202 -0
  18. data/lib/httpx/plugins/expect.rb +4 -3
  19. data/lib/httpx/plugins/follow_redirects.rb +7 -8
  20. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  21. data/lib/httpx/plugins/h2c.rb +23 -20
  22. data/lib/httpx/plugins/internal_telemetry.rb +27 -0
  23. data/lib/httpx/plugins/persistent.rb +16 -0
  24. data/lib/httpx/plugins/proxy/http.rb +17 -19
  25. data/lib/httpx/plugins/proxy.rb +91 -93
  26. data/lib/httpx/plugins/retries.rb +5 -8
  27. data/lib/httpx/plugins/upgrade.rb +5 -10
  28. data/lib/httpx/plugins/webdav.rb +6 -0
  29. data/lib/httpx/plugins/xml.rb +76 -0
  30. data/lib/httpx/pool.rb +73 -244
  31. data/lib/httpx/request/body.rb +25 -26
  32. data/lib/httpx/request.rb +7 -1
  33. data/lib/httpx/resolver/https.rb +15 -20
  34. data/lib/httpx/resolver/multi.rb +34 -16
  35. data/lib/httpx/resolver/native.rb +66 -25
  36. data/lib/httpx/resolver/resolver.rb +59 -15
  37. data/lib/httpx/resolver/system.rb +31 -15
  38. data/lib/httpx/resolver.rb +21 -14
  39. data/lib/httpx/response.rb +5 -3
  40. data/lib/httpx/selector.rb +160 -95
  41. data/lib/httpx/session.rb +273 -140
  42. data/lib/httpx/transcoder/body.rb +15 -31
  43. data/lib/httpx/transcoder/gzip.rb +0 -3
  44. data/lib/httpx/transcoder/json.rb +14 -2
  45. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  46. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  47. data/lib/httpx/transcoder/utils/inflater.rb +2 -0
  48. data/lib/httpx/transcoder.rb +0 -1
  49. data/lib/httpx/version.rb +1 -1
  50. data/lib/httpx.rb +20 -21
  51. data/sig/callbacks.rbs +0 -1
  52. data/sig/chainable.rbs +4 -0
  53. data/sig/connection/http2.rbs +1 -1
  54. data/sig/connection.rbs +29 -3
  55. data/sig/errors.rbs +6 -0
  56. data/sig/loggable.rbs +2 -0
  57. data/sig/options.rbs +7 -0
  58. data/sig/plugins/aws_sigv4.rbs +8 -2
  59. data/sig/plugins/content_digest.rbs +51 -0
  60. data/sig/plugins/cookies/cookie.rbs +9 -0
  61. data/sig/plugins/grpc/call.rbs +4 -0
  62. data/sig/plugins/persistent.rbs +4 -1
  63. data/sig/plugins/proxy/socks5.rbs +11 -3
  64. data/sig/plugins/proxy.rbs +18 -11
  65. data/sig/plugins/push_promise.rbs +3 -0
  66. data/sig/plugins/rate_limiter.rbs +2 -0
  67. data/sig/plugins/retries.rbs +1 -1
  68. data/sig/plugins/ssrf_filter.rbs +26 -0
  69. data/sig/plugins/webdav.rbs +23 -0
  70. data/sig/plugins/xml.rbs +37 -0
  71. data/sig/pool.rbs +25 -33
  72. data/sig/request/body.rbs +5 -9
  73. data/sig/resolver/multi.rbs +26 -1
  74. data/sig/resolver/native.rbs +2 -2
  75. data/sig/resolver/resolver.rbs +21 -2
  76. data/sig/resolver.rbs +5 -1
  77. data/sig/response/buffer.rbs +1 -1
  78. data/sig/selector.rbs +30 -4
  79. data/sig/session.rbs +47 -18
  80. data/sig/transcoder/body.rbs +2 -4
  81. data/sig/transcoder/chunker.rbs +1 -1
  82. data/sig/transcoder/deflate.rbs +1 -0
  83. data/sig/transcoder/form.rbs +8 -0
  84. data/sig/transcoder/gzip.rbs +4 -1
  85. data/sig/transcoder/utils/body_reader.rbs +3 -3
  86. data/sig/transcoder/utils/deflater.rbs +3 -3
  87. metadata +12 -4
  88. data/lib/httpx/transcoder/xml.rb +0 -52
  89. data/sig/transcoder/xml.rbs +0 -22
data/lib/httpx/pool.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
3
  require "httpx/selector"
5
4
  require "httpx/connection"
6
5
  require "httpx/resolver"
@@ -8,110 +7,31 @@ require "httpx/resolver"
8
7
  module HTTPX
9
8
  class Pool
10
9
  using ArrayExtensions::FilterMap
11
- extend Forwardable
10
+ using URIExtensions
12
11
 
13
- def_delegator :@timers, :after
12
+ POOL_TIMEOUT = 5
14
13
 
15
- def initialize
16
- @resolvers = {}
17
- @timers = Timers.new
18
- @selector = Selector.new
19
- @connections = []
20
- end
21
-
22
- def wrap
23
- connections = @connections
14
+ # Sets up the connection pool with the given +options+, which can be the following:
15
+ #
16
+ # :max_connections_per_origin :: the maximum number of connections held in the pool pointing to a given origin.
17
+ # :pool_timeout :: the number of seconds to wait for a connection to a given origin (before raising HTTPX::PoolTimeoutError)
18
+ #
19
+ def initialize(options)
20
+ @max_connections_per_origin = options.fetch(:max_connections_per_origin, Float::INFINITY)
21
+ @pool_timeout = options.fetch(:pool_timeout, POOL_TIMEOUT)
22
+ @resolvers = Hash.new { |hs, resolver_type| hs[resolver_type] = [] }
23
+ @resolver_mtx = Thread::Mutex.new
24
24
  @connections = []
25
-
26
- begin
27
- yield self
28
- ensure
29
- @connections.unshift(*connections)
30
- end
31
- end
32
-
33
- def empty?
34
- @connections.empty?
35
- end
36
-
37
- def next_tick
38
- catch(:jump_tick) do
39
- timeout = next_timeout
40
- if timeout && timeout.negative?
41
- @timers.fire
42
- throw(:jump_tick)
43
- end
44
-
45
- begin
46
- @selector.select(timeout, &:call)
47
- @timers.fire
48
- rescue TimeoutError => e
49
- @timers.fire(e)
50
- end
51
- end
52
- rescue StandardError => e
53
- @connections.each do |connection|
54
- connection.emit(:error, e)
55
- end
56
- rescue Exception # rubocop:disable Lint/RescueException
57
- @connections.each(&:force_reset)
58
- raise
59
- end
60
-
61
- def close(connections = @connections)
62
- return if connections.empty?
63
-
64
- connections = connections.reject(&:inflight?)
65
- connections.each(&:terminate)
66
- next_tick until connections.none? { |c| c.state != :idle && @connections.include?(c) }
67
-
68
- # close resolvers
69
- outstanding_connections = @connections
70
- resolver_connections = @resolvers.each_value.flat_map(&:connections).compact
71
- outstanding_connections -= resolver_connections
72
-
73
- return unless outstanding_connections.empty?
74
-
75
- @resolvers.each_value do |resolver|
76
- resolver.close unless resolver.closed?
77
- end
78
- # for https resolver
79
- resolver_connections.each(&:terminate)
80
- next_tick until resolver_connections.none? { |c| c.state != :idle && @connections.include?(c) }
81
- end
82
-
83
- def init_connection(connection, _options)
84
- connection.timers = @timers
85
- connection.on(:activate) do
86
- select_connection(connection)
87
- end
88
- connection.on(:exhausted) do
89
- case connection.state
90
- when :closed
91
- connection.idling
92
- @connections << connection
93
- select_connection(connection)
94
- when :closing
95
- connection.once(:close) do
96
- connection.idling
97
- @connections << connection
98
- select_connection(connection)
99
- end
100
- end
101
- end
102
- connection.on(:close) do
103
- unregister_connection(connection)
104
- end
105
- connection.on(:terminate) do
106
- unregister_connection(connection, true)
107
- end
108
- resolve_connection(connection) unless connection.family
25
+ @connection_mtx = Thread::Mutex.new
26
+ @origin_counters = Hash.new(0)
27
+ @origin_conds = Hash.new { |hs, orig| hs[orig] = ConditionVariable.new }
109
28
  end
110
29
 
111
- def deactivate(*connections)
112
- connections.each do |connection|
113
- connection.deactivate
114
- deselect_connection(connection) if connection.state == :inactive
30
+ def pop_connection
31
+ @connection_mtx.synchronize do
32
+ conn = @connections.shift
33
+ @origin_conds.delete(conn.origin) if conn && (@origin_counters[conn.origin.to_s] -= 1).zero?
34
+ conn
115
35
  end
116
36
  end
117
37
 
@@ -119,185 +39,94 @@ module HTTPX
119
39
  # Many hostnames are reachable through the same IP, so we try to
120
40
  # maximize pipelining by opening as few connections as possible.
121
41
  #
122
- def find_connection(uri, options)
123
- conn = @connections.find do |connection|
124
- connection.match?(uri, options)
125
- end
42
+ def checkout_connection(uri, options)
43
+ return checkout_new_connection(uri, options) if options.io
126
44
 
127
- return unless conn
45
+ @connection_mtx.synchronize do
46
+ acquire_connection(uri, options) || begin
47
+ if @origin_counters[uri.origin] == @max_connections_per_origin
128
48
 
129
- case conn.state
130
- when :closed
131
- conn.idling
132
- select_connection(conn)
133
- when :closing
134
- conn.once(:close) do
135
- conn.idling
136
- select_connection(conn)
137
- end
138
- end
49
+ @origin_conds[uri.origin].wait(@connection_mtx, @pool_timeout)
139
50
 
140
- conn
141
- end
142
-
143
- private
144
-
145
- def resolve_connection(connection)
146
- @connections << connection unless @connections.include?(connection)
147
-
148
- if connection.addresses || connection.open?
149
- #
150
- # there are two cases in which we want to activate initialization of
151
- # connection immediately:
152
- #
153
- # 1. when the connection already has addresses, i.e. it doesn't need to
154
- # resolve a name (not the same as name being an IP, yet)
155
- # 2. when the connection is initialized with an external already open IO.
156
- #
157
- connection.once(:connect_error, &connection.method(:handle_error))
158
- on_resolver_connection(connection)
159
- return
160
- end
51
+ return acquire_connection(uri, options) || raise(PoolTimeoutError.new(uri.origin, @pool_timeout))
52
+ end
161
53
 
162
- find_resolver_for(connection) do |resolver|
163
- resolver << try_clone_connection(connection, resolver.family)
164
- next if resolver.empty?
54
+ @origin_counters[uri.origin] += 1
165
55
 
166
- select_connection(resolver)
56
+ checkout_new_connection(uri, options)
57
+ end
167
58
  end
168
59
  end
169
60
 
170
- def try_clone_connection(connection, family)
171
- connection.family ||= family
172
-
173
- return connection if connection.family == family
174
-
175
- new_connection = connection.class.new(connection.origin, connection.options)
176
- new_connection.family = family
61
+ def checkin_connection(connection)
62
+ return if connection.options.io
177
63
 
178
- connection.once(:tcp_open) { new_connection.force_reset }
179
- connection.once(:connect_error) do |err|
180
- if new_connection.connecting?
181
- new_connection.merge(connection)
182
- connection.emit(:cloned, new_connection)
183
- connection.force_reset
184
- else
185
- connection.__send__(:handle_error, err)
186
- end
187
- end
64
+ @connection_mtx.synchronize do
65
+ @connections << connection
188
66
 
189
- new_connection.once(:tcp_open) do |new_conn|
190
- if new_conn != connection
191
- new_conn.merge(connection)
192
- connection.force_reset
193
- end
194
- end
195
- new_connection.once(:connect_error) do |err|
196
- if connection.connecting?
197
- # main connection has the requests
198
- connection.merge(new_connection)
199
- new_connection.emit(:cloned, connection)
200
- new_connection.force_reset
201
- else
202
- new_connection.__send__(:handle_error, err)
203
- end
67
+ @origin_conds[connection.origin.to_s].signal
204
68
  end
205
-
206
- init_connection(new_connection, connection.options)
207
- new_connection
208
69
  end
209
70
 
210
- def on_resolver_connection(connection)
211
- @connections << connection unless @connections.include?(connection)
212
- found_connection = @connections.find do |ch|
213
- ch != connection && ch.mergeable?(connection)
214
- end
215
- return register_connection(connection) unless found_connection
71
+ def checkout_mergeable_connection(connection)
72
+ return if connection.options.io
216
73
 
217
- if found_connection.open?
218
- coalesce_connections(found_connection, connection)
219
- throw(:coalesced, found_connection) unless @connections.include?(connection)
220
- else
221
- found_connection.once(:open) do
222
- coalesce_connections(found_connection, connection)
74
+ @connection_mtx.synchronize do
75
+ idx = @connections.find_index do |ch|
76
+ ch != connection && ch.mergeable?(connection)
223
77
  end
78
+ @connections.delete_at(idx) if idx
224
79
  end
225
80
  end
226
81
 
227
- def on_resolver_error(connection, error)
228
- return connection.emit(:connect_error, error) if connection.connecting? && connection.callbacks_for?(:connect_error)
229
-
230
- connection.emit(:error, error)
82
+ def reset_resolvers
83
+ @resolver_mtx.synchronize { @resolvers.clear }
231
84
  end
232
85
 
233
- def on_resolver_close(resolver)
234
- resolver_type = resolver.class
235
- return if resolver.closed?
86
+ def checkout_resolver(options)
87
+ resolver_type = options.resolver_class
88
+ resolver_type = Resolver.resolver_for(resolver_type)
236
89
 
237
- @resolvers.delete(resolver_type)
90
+ @resolver_mtx.synchronize do
91
+ resolvers = @resolvers[resolver_type]
238
92
 
239
- deselect_connection(resolver)
240
- resolver.close unless resolver.closed?
93
+ idx = resolvers.find_index do |res|
94
+ res.options == options
95
+ end
96
+ resolvers.delete_at(idx) if idx
97
+ end || checkout_new_resolver(resolver_type, options)
241
98
  end
242
99
 
243
- def register_connection(connection)
244
- select_connection(connection)
245
- end
100
+ def checkin_resolver(resolver)
101
+ @resolver_mtx.synchronize do
102
+ resolvers = @resolvers[resolver.class]
246
103
 
247
- def unregister_connection(connection, cleanup = !connection.used?)
248
- @connections.delete(connection) if cleanup
249
- deselect_connection(connection)
250
- end
104
+ resolver = resolver.multi
251
105
 
252
- def select_connection(connection)
253
- @selector.register(connection)
106
+ resolvers << resolver unless resolvers.include?(resolver)
107
+ end
254
108
  end
255
109
 
256
- def deselect_connection(connection)
257
- @selector.deregister(connection)
258
- end
110
+ private
259
111
 
260
- def coalesce_connections(conn1, conn2)
261
- return register_connection(conn2) unless conn1.coalescable?(conn2)
112
+ def acquire_connection(uri, options)
113
+ idx = @connections.find_index do |connection|
114
+ connection.match?(uri, options)
115
+ end
262
116
 
263
- conn2.emit(:tcp_open, conn1)
264
- conn1.merge(conn2)
265
- @connections.delete(conn2)
117
+ @connections.delete_at(idx) if idx
266
118
  end
267
119
 
268
- def next_timeout
269
- [
270
- @timers.wait_interval,
271
- *@resolvers.values.reject(&:closed?).filter_map(&:timeout),
272
- *@connections.filter_map(&:timeout),
273
- ].compact.min
120
+ def checkout_new_connection(uri, options)
121
+ options.connection_class.new(uri, options)
274
122
  end
275
123
 
276
- def find_resolver_for(connection)
277
- connection_options = connection.options
278
- resolver_type = connection_options.resolver_class
279
- resolver_type = Resolver.resolver_for(resolver_type)
280
-
281
- @resolvers[resolver_type] ||= begin
282
- resolver_manager = if resolver_type.multi?
283
- Resolver::Multi.new(resolver_type, connection_options)
284
- else
285
- resolver_type.new(connection_options)
286
- end
287
- resolver_manager.on(:resolve, &method(:on_resolver_connection))
288
- resolver_manager.on(:error, &method(:on_resolver_error))
289
- resolver_manager.on(:close, &method(:on_resolver_close))
290
- resolver_manager
291
- end
292
-
293
- manager = @resolvers[resolver_type]
294
-
295
- (manager.is_a?(Resolver::Multi) && manager.early_resolve(connection)) || manager.resolvers.each do |resolver|
296
- resolver.pool = self
297
- yield resolver
124
+ def checkout_new_resolver(resolver_type, options)
125
+ if resolver_type.multi?
126
+ Resolver::Multi.new(resolver_type, options)
127
+ else
128
+ resolver_type.new(options)
298
129
  end
299
-
300
- manager
301
130
  end
302
131
  end
303
132
  end
@@ -25,20 +25,11 @@ module HTTPX
25
25
  # ..., form: { foo: Pathname.open("path/to/file") }) #=> multipart urlencoded encoder
26
26
  # ..., form: { foo: File.open("path/to/file") }) #=> multipart urlencoded encoder
27
27
  # ..., form: { body: "bla") }) #=> raw data encoder
28
- def initialize(headers, options, body: nil, form: nil, json: nil, xml: nil, **params)
29
- @headers = headers
28
+ def initialize(h, options, **params)
29
+ @headers = h
30
+ @body = self.class.initialize_body(params)
30
31
  @options = options.merge(params)
31
32
 
32
- @body = if body
33
- Transcoder::Body.encode(body)
34
- elsif form
35
- Transcoder::Form.encode(form)
36
- elsif json
37
- Transcoder::JSON.encode(json)
38
- elsif xml
39
- Transcoder::Xml.encode(xml)
40
- end
41
-
42
33
  if @body
43
34
  if @options.compress_request_body && @headers.key?("content-encoding")
44
35
 
@@ -61,7 +52,11 @@ module HTTPX
61
52
 
62
53
  body = stream(@body)
63
54
  if body.respond_to?(:read)
64
- ::IO.copy_stream(body, ProcIO.new(block))
55
+ while (chunk = body.read(16_384))
56
+ block.call(chunk)
57
+ end
58
+ # TODO: use copy_stream once bug is resolved: https://bugs.ruby-lang.org/issues/21131
59
+ # ::IO.copy_stream(body, ProcIO.new(block))
65
60
  elsif body.respond_to?(:each)
66
61
  body.each(&block)
67
62
  else
@@ -69,6 +64,10 @@ module HTTPX
69
64
  end
70
65
  end
71
66
 
67
+ def close
68
+ @body.close if @body.respond_to?(:close)
69
+ end
70
+
72
71
  # if the +@body+ is rewindable, it rewinnds it.
73
72
  def rewind
74
73
  return if empty?
@@ -123,6 +122,19 @@ module HTTPX
123
122
  # :nocov:
124
123
 
125
124
  class << self
125
+ def initialize_body(params)
126
+ if (body = params.delete(:body))
127
+ # @type var body: bodyIO
128
+ Transcoder::Body.encode(body)
129
+ elsif (form = params.delete(:form))
130
+ # @type var form: Transcoder::urlencoded_input
131
+ Transcoder::Form.encode(form)
132
+ elsif (json = params.delete(:json))
133
+ # @type var body: _ToJson
134
+ Transcoder::JSON.encode(json)
135
+ end
136
+ end
137
+
126
138
  # returns the +body+ wrapped with the correct deflater accordinng to the given +encodisng+.
127
139
  def initialize_deflater_body(body, encoding)
128
140
  case encoding
@@ -138,17 +150,4 @@ module HTTPX
138
150
  end
139
151
  end
140
152
  end
141
-
142
- # Wrapper yielder which can be used with functions which expect an IO writer.
143
- class ProcIO
144
- def initialize(block)
145
- @block = block
146
- end
147
-
148
- # Implementation the IO write protocol, which yield the given chunk to +@block+.
149
- def write(data)
150
- @block.call(data.dup)
151
- data.bytesize
152
- end
153
- end
154
153
  end
data/lib/httpx/request.rb CHANGED
@@ -11,8 +11,10 @@ module HTTPX
11
11
  include Callbacks
12
12
  using URIExtensions
13
13
 
14
+ ALLOWED_URI_SCHEMES = %w[https http].freeze
15
+
14
16
  # default value used for "user-agent" header, when not overridden.
15
- USER_AGENT = "httpx.rb/#{VERSION}"
17
+ USER_AGENT = "httpx.rb/#{VERSION}".freeze # rubocop:disable Style/RedundantFreeze
16
18
 
17
19
  # the upcased string HTTP verb for this request.
18
20
  attr_reader :verb
@@ -92,6 +94,8 @@ module HTTPX
92
94
  @uri = origin.merge("#{base_path}#{@uri}")
93
95
  end
94
96
 
97
+ raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
98
+
95
99
  @state = :idle
96
100
  @response = nil
97
101
  @peer_address = nil
@@ -263,6 +267,8 @@ module HTTPX
263
267
  return unless @state == :body
264
268
  when :done
265
269
  return if @state == :expect
270
+
271
+ @body.close
266
272
  end
267
273
  @state = nextstate
268
274
  emit(@state, self)
@@ -27,7 +27,7 @@ module HTTPX
27
27
  use_get: false,
28
28
  }.freeze
29
29
 
30
- def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate
30
+ def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close, :terminate, :inflight?
31
31
 
32
32
  def initialize(_, options)
33
33
  super
@@ -43,7 +43,7 @@ module HTTPX
43
43
  end
44
44
 
45
45
  def <<(connection)
46
- return if @uri.origin == connection.origin.to_s
46
+ return if @uri.origin == connection.peer.to_s
47
47
 
48
48
  @uri_addresses ||= HTTPX::Resolver.nolookup_resolve(@uri.host) || @resolver.getaddresses(@uri.host)
49
49
 
@@ -66,30 +66,25 @@ module HTTPX
66
66
  end
67
67
 
68
68
  def resolver_connection
69
- @resolver_connection ||= @pool.find_connection(@uri, @options) || begin
70
- @building_connection = true
71
- connection = @options.connection_class.new(@uri, @options.merge(ssl: { alpn_protocols: %w[h2] }))
72
- @pool.init_connection(connection, @options)
73
- # only explicity emit addresses if connection didn't pre-resolve, i.e. it's not an IP.
74
- catch(:coalesced) do
75
- @building_connection = false
76
- emit_addresses(connection, @family, @uri_addresses) unless connection.addresses
77
- connection
78
- end
69
+ # TODO: leaks connection object into the pool
70
+ @resolver_connection ||= @current_session.find_connection(@uri, @current_selector,
71
+ @options.merge(ssl: { alpn_protocols: %w[h2] })).tap do |conn|
72
+ emit_addresses(conn, @family, @uri_addresses) unless conn.addresses
79
73
  end
80
74
  end
81
75
 
82
76
  private
83
77
 
84
78
  def resolve(connection = @connections.first, hostname = nil)
85
- return if @building_connection
86
79
  return unless connection
87
80
 
88
81
  hostname ||= @queries.key(connection)
89
82
 
90
83
  if hostname.nil?
91
- hostname = connection.origin.host
92
- log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
84
+ hostname = connection.peer.host
85
+ log do
86
+ "resolver #{FAMILY_TYPES[@record_type]}: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}"
87
+ end if connection.peer.non_ascii_hostname
93
88
 
94
89
  hostname = @resolver.generate_candidates(hostname).each do |name|
95
90
  @queries[name.to_s] = connection
@@ -97,7 +92,7 @@ module HTTPX
97
92
  else
98
93
  @queries[hostname] = connection
99
94
  end
100
- log { "resolver: query #{FAMILY_TYPES[RECORD_TYPES[@family]]} for #{hostname}" }
95
+ log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
101
96
 
102
97
  begin
103
98
  request = build_request(hostname)
@@ -108,7 +103,7 @@ module HTTPX
108
103
  @connections << connection
109
104
  rescue ResolveError, Resolv::DNS::EncodeError => e
110
105
  reset_hostname(hostname)
111
- emit_resolve_error(connection, connection.origin.host, e)
106
+ emit_resolve_error(connection, connection.peer.host, e)
112
107
  end
113
108
  end
114
109
 
@@ -117,7 +112,7 @@ module HTTPX
117
112
  rescue StandardError => e
118
113
  hostname = @requests.delete(request)
119
114
  connection = reset_hostname(hostname)
120
- emit_resolve_error(connection, connection.origin.host, e)
115
+ emit_resolve_error(connection, connection.peer.host, e)
121
116
  else
122
117
  # @type var response: HTTPX::Response
123
118
  parse(request, response)
@@ -156,7 +151,7 @@ module HTTPX
156
151
  when :decode_error
157
152
  host = @requests.delete(request)
158
153
  connection = reset_hostname(host)
159
- emit_resolve_error(connection, connection.origin.host, result)
154
+ emit_resolve_error(connection, connection.peer.host, result)
160
155
  end
161
156
  end
162
157
 
@@ -176,7 +171,7 @@ module HTTPX
176
171
  alias_address = answers[address["alias"]]
177
172
  if alias_address.nil?
178
173
  reset_hostname(address["name"])
179
- if catch(:coalesced) { early_resolve(connection, hostname: address["alias"]) }
174
+ if early_resolve(connection, hostname: address["alias"])
180
175
  @connections.delete(connection)
181
176
  else
182
177
  resolve(connection, address["alias"])
@@ -8,27 +8,45 @@ module HTTPX
8
8
  include Callbacks
9
9
  using ArrayExtensions::FilterMap
10
10
 
11
- attr_reader :resolvers
11
+ attr_reader :resolvers, :options
12
12
 
13
13
  def initialize(resolver_type, options)
14
+ @current_selector = nil
15
+ @current_session = nil
14
16
  @options = options
15
17
  @resolver_options = @options.resolver_options
16
18
 
17
19
  @resolvers = options.ip_families.map do |ip_family|
18
20
  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) }
21
+ resolver.multi = self
22
22
  resolver
23
23
  end
24
24
 
25
25
  @errors = Hash.new { |hs, k| hs[k] = [] }
26
26
  end
27
27
 
28
+ def current_selector=(s)
29
+ @current_selector = s
30
+ @resolvers.each { |r| r.__send__(__method__, s) }
31
+ end
32
+
33
+ def current_session=(s)
34
+ @current_session = s
35
+ @resolvers.each { |r| r.__send__(__method__, s) }
36
+ end
37
+
28
38
  def closed?
29
39
  @resolvers.all?(&:closed?)
30
40
  end
31
41
 
42
+ def empty?
43
+ @resolvers.all?(&:empty?)
44
+ end
45
+
46
+ def inflight?
47
+ @resolvers.any(&:inflight?)
48
+ end
49
+
32
50
  def timeout
33
51
  @resolvers.filter_map(&:timeout).min
34
52
  end
@@ -42,10 +60,11 @@ module HTTPX
42
60
  end
43
61
 
44
62
  def early_resolve(connection)
45
- hostname = connection.origin.host
63
+ hostname = connection.peer.host
46
64
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
47
- return unless addresses
65
+ return false unless addresses
48
66
 
67
+ resolved = false
49
68
  addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
50
69
  # try to match the resolver by family. However, there are cases where that's not possible, as when
51
70
  # the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
@@ -55,21 +74,20 @@ module HTTPX
55
74
 
56
75
  # it does not matter which resolver it is, as early-resolve code is shared.
57
76
  resolver.emit_addresses(connection, family, addrs, true)
58
- end
59
- end
60
77
 
61
- private
78
+ resolved = true
79
+ end
62
80
 
63
- def on_resolver_connection(connection)
64
- emit(:resolve, connection)
81
+ resolved
65
82
  end
66
83
 
67
- def on_resolver_error(connection, error)
68
- emit(:error, connection, error)
69
- end
84
+ def lazy_resolve(connection)
85
+ @resolvers.each do |resolver|
86
+ resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
87
+ next if resolver.empty?
70
88
 
71
- def on_resolver_close(resolver)
72
- emit(:close, resolver)
89
+ @current_session.select_resolver(resolver, @current_selector)
90
+ end
73
91
  end
74
92
  end
75
93
  end