httpx 0.18.7 → 0.19.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -1
  3. data/doc/release_notes/0_19_0.md +39 -0
  4. data/doc/release_notes/0_19_1.md +5 -0
  5. data/doc/release_notes/0_19_2.md +7 -0
  6. data/doc/release_notes/0_19_3.md +6 -0
  7. data/lib/httpx/adapters/faraday.rb +7 -3
  8. data/lib/httpx/connection.rb +14 -10
  9. data/lib/httpx/extensions.rb +16 -0
  10. data/lib/httpx/headers.rb +0 -2
  11. data/lib/httpx/io/tcp.rb +24 -5
  12. data/lib/httpx/options.rb +44 -11
  13. data/lib/httpx/plugins/cookies.rb +5 -7
  14. data/lib/httpx/plugins/proxy.rb +2 -5
  15. data/lib/httpx/plugins/retries.rb +2 -2
  16. data/lib/httpx/pool.rb +40 -20
  17. data/lib/httpx/resolver/https.rb +32 -42
  18. data/lib/httpx/resolver/multi.rb +79 -0
  19. data/lib/httpx/resolver/native.rb +24 -39
  20. data/lib/httpx/resolver/resolver.rb +95 -0
  21. data/lib/httpx/resolver/system.rb +175 -19
  22. data/lib/httpx/resolver.rb +37 -11
  23. data/lib/httpx/response.rb +4 -2
  24. data/lib/httpx/session_extensions.rb +9 -2
  25. data/lib/httpx/timers.rb +1 -1
  26. data/lib/httpx/transcoder/chunker.rb +0 -1
  27. data/lib/httpx/version.rb +1 -1
  28. data/lib/httpx.rb +2 -0
  29. data/sig/errors.rbs +8 -0
  30. data/sig/headers.rbs +0 -2
  31. data/sig/httpx.rbs +4 -0
  32. data/sig/options.rbs +10 -7
  33. data/sig/parser/http1.rbs +14 -5
  34. data/sig/pool.rbs +17 -9
  35. data/sig/registry.rbs +3 -0
  36. data/sig/request.rbs +11 -0
  37. data/sig/resolver/https.rbs +15 -27
  38. data/sig/resolver/multi.rbs +7 -0
  39. data/sig/resolver/native.rbs +3 -12
  40. data/sig/resolver/resolver.rbs +36 -0
  41. data/sig/resolver/system.rbs +3 -9
  42. data/sig/resolver.rbs +12 -10
  43. data/sig/response.rbs +15 -5
  44. data/sig/selector.rbs +3 -3
  45. data/sig/timers.rbs +5 -2
  46. data/sig/transcoder/chunker.rbs +16 -5
  47. data/sig/transcoder/json.rbs +5 -0
  48. data/sig/transcoder.rbs +3 -1
  49. metadata +14 -4
  50. data/lib/httpx/resolver/resolver_mixin.rb +0 -75
  51. data/sig/resolver/resolver_mixin.rbs +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab9b9577b0e266b140e21030307b3f2436af346a13f1e0931af05020ba42f7e6
4
- data.tar.gz: c5d043448ef41b0532c08d1622540d80c68511ef59cffcc810a6f8bd92f348d5
3
+ metadata.gz: 431fcabfda42f5d6010d903a15c6a83357123c9eef3528a769755c8639f1ec61
4
+ data.tar.gz: 52512337b7b2081a4ab123f7261dee14adb97da019fc9244b9ddf8e7de53c904
5
5
  SHA512:
6
- metadata.gz: 8b4ec58f2ab031d23f28a86bc47b40d0f0d79eea9aea64590b0cc3d07269738513de1364608e0c0282a6956d816b78b900520dbad4daab1f73522b8b7f73d138
7
- data.tar.gz: f2d2c297b530bfb3567ebcd1678bad1d729187dd75c9107839c925def1a708c4648fe80df614a01af02bce309594e256e274e042f3c6b8347182aebff1df9935
6
+ metadata.gz: 7777af904a2e5a8f6f34984a69cb022c853483b6634dd631a3fbe92c9005fadb3637a21a214ff1e14ef4729ffa70d68362b703b9ccd25e2904b820fc674dbe6b
7
+ data.tar.gz: 94d92ad004319ecb3098b5e901921bfaf5d818cbb0e2e87eed8987248d1cc52d0fc70a5dc1dfe1a92faa8eae9e7074c926c0c01563002cb8067af449ae79fd51
data/README.md CHANGED
@@ -140,11 +140,15 @@ In order to use HTTP/2 under JRuby, [check this link](https://gitlab.com/honeyry
140
140
  * Doesn't work with ruby 2.4.0 for Windows (see [#36](https://gitlab.com/honeyryderchuck/httpx/issues/36)).
141
141
  * Using `total_timeout` along with the `:persistent` plugin [does not work as you might expect](https://gitlab.com/honeyryderchuck/httpx/-/wikis/Timeouts#total_timeout).
142
142
 
143
+ ## Versioning Policy
144
+
145
+ Although 0.x software, `httpx` is considered API-stable and production-ready, i.e. current API or options may be subject to deprecation and emit log warnings, but can only effectively be removed in a major version change.
146
+
143
147
  ## Contributing
144
148
 
145
149
  * Discuss your contribution in an issue
146
150
  * Fork it
147
151
  * Make your changes, add some tests
148
- * Ensure all tests pass (`bundle exec rake test`)
152
+ * Ensure all tests pass (`docker-compose -f docker-compose.yml -f docker-compose-ruby-{RUBY_VERSION}.yml run httpx bundle exec rake test`)
149
153
  * Open a Merge Request (that's Pull Request in Github-ish)
150
154
  * Wait for feedback
@@ -0,0 +1,39 @@
1
+ # 0.19.0
2
+
3
+ ## Features
4
+
5
+ ### Happy Eyeballs v2
6
+
7
+ When the system supports dual-stack networking, `httpx` implements the Happy Eyeballs v2 algorithm (RFC 8305) to resolve hostnames to both IPv6 and IPv4 addresses while privileging IPv6 connectivity. This is implemented by `httpx` both for the `:native` as well as the `:https` (DoH) resolver (which do not perform address sorting, thereby being "DNS-based load-balancing" friendly), and "outsourced" to `getaddrinfo` when using the `:system` resolver.
8
+
9
+ IPv6 connectivity will also be privileged for `/etc/hosts` local DNS (i.e. `localhost` connections will connec to `::1`).
10
+
11
+ A new option, `:ip_families`, will also be available (`[Socket::AF_INET6, Socket::AF_INET]` in dual-stack systems). If you'd like to i.e. force IPv4 connectivity, you can do use it (`client = HTTPX.with(ip_families: [Socket::AF_INET])`).
12
+
13
+ ## Improvements
14
+
15
+ ### DNS: :system resolver uses getaddrinfo (instead of the resolver lib)
16
+
17
+ The `:system` resolver switched to using the `getaddinfo` system function to perform DNS requests. Not only is this call **not** blocking the session event loop anymore (unlike pre-0.19.0 `:system` resolver), it adds a lot of functionality that the stdlib `resolv` library just doesn't support at the moment (such as SRV records).
18
+
19
+ ### HTTP/2 proxy support
20
+
21
+ The `:proxy` plugin handles "prior-knowledge" HTTP/2 proxies.
22
+
23
+ ```ruby
24
+ HTTPX.plugin(:proxy, fallback_protocol: "h2").with_proxy(uri: "http://http2-proxy:3128").get(...
25
+ ```
26
+
27
+ Connection coalescing has also been enabled for proxied connections (also `CONNECT`-tunneled connections).
28
+
29
+ ### curl-to-httpx
30
+
31
+ widget in [project website](https://honeyryderchuck.gitlab.io/httpx/) to turn curl commands into the equivalent `httpx` code.
32
+
33
+ ## Bugfixes
34
+
35
+ * faraday adapter now supports passing session options.
36
+ * proxy: several fixes which enabled env-var (`HTTP(S)_PROXY`) defined proxy support.
37
+ * proxy: fixed graceful recovery from proxy tcp connect errors.
38
+ * several fixes around CNAMEs timeouts with the native resolver.
39
+ * https resolver is now closed when wrapping session closes (it was left open).
@@ -0,0 +1,5 @@
1
+ # 0.19.1
2
+
3
+ ## Bugfixes
4
+
5
+ Fixing a DNS dual-stack case where one the resolvers may have finished way before the previous one and will therefore return no timeout.
@@ -0,0 +1,7 @@
1
+ # 0.19.2
2
+
3
+ ## Bugfixes
4
+
5
+ * skip resolution delay path for early resolve cases
6
+
7
+ when the early resolve path (using IP, /etc/hosts IP, IP from cache) is followed, emit_addresses is called, and in a particular case (dual-stack network but using an IPv4 address), the happy eyeballs resolution delay path was activated when it shouldn't (it's meant to be used only for DNS network requests), and resulted in @pool being called before it was ever set. This simple check ensures that it doesn't happen before it must.
@@ -0,0 +1,6 @@
1
+ # 0.19.3
2
+
3
+ ## Bugfixes
4
+
5
+ * `retries` plugin: allow passing floats to `:retry_after` option.
6
+ * dns: fixing cache lookups filtering by IP family which was causing socket connect handshake to start with no IP.
@@ -81,6 +81,9 @@ module Faraday
81
81
 
82
82
  def response=(response)
83
83
  super
84
+
85
+ return if response.is_a?(::HTTPX::ErrorResponse)
86
+
84
87
  response.body.on_data = @response_on_data
85
88
  end
86
89
  end
@@ -136,7 +139,7 @@ module Faraday
136
139
 
137
140
  def on_response(&blk)
138
141
  if blk
139
- @on_response = lambda do |response|
142
+ @on_response = ->(response) do
140
143
  blk.call(response)
141
144
  end
142
145
  self
@@ -200,9 +203,9 @@ module Faraday
200
203
  end
201
204
  end
202
205
 
203
- def initialize(app)
206
+ def initialize(app, options = {})
204
207
  super(app)
205
- @session = Session.new
208
+ @session = Session.new(options)
206
209
  end
207
210
 
208
211
  def call(env)
@@ -210,6 +213,7 @@ module Faraday
210
213
  if parallel?(env)
211
214
  handler = env[:parallel_manager].enqueue(env)
212
215
  handler.on_response do |response|
216
+ response.raise_for_status
213
217
  save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
214
218
  response_headers.merge!(response.headers)
215
219
  end
@@ -44,7 +44,7 @@ module HTTPX
44
44
 
45
45
  def_delegator :@write_buffer, :empty?
46
46
 
47
- attr_reader :origin, :origins, :state, :pending, :options
47
+ attr_reader :io, :origin, :origins, :state, :pending, :options
48
48
 
49
49
  attr_writer :timers
50
50
 
@@ -78,7 +78,11 @@ module HTTPX
78
78
  # this is a semi-private method, to be used by the resolver
79
79
  # to initiate the io object.
80
80
  def addresses=(addrs)
81
- @io ||= IO.registry(@type).new(@origin, addrs, @options) # rubocop:disable Naming/MemoizedInstanceVariableName
81
+ if @io
82
+ @io.add_addresses(addrs)
83
+ else
84
+ @io = IO.registry(@type).new(@origin, addrs, @options)
85
+ end
82
86
  end
83
87
 
84
88
  def addresses
@@ -490,14 +494,14 @@ module HTTPX
490
494
 
491
495
  def transition(nextstate)
492
496
  handle_transition(nextstate)
493
- rescue Errno::ECONNREFUSED,
494
- Errno::EADDRNOTAVAIL,
495
- Errno::EHOSTUNREACH,
496
- TLSError => e
497
- # connect errors, exit gracefully
498
- handle_error(e)
499
- @state = :closed
500
- emit(:close)
497
+ rescue Errno::ECONNREFUSED,
498
+ Errno::EADDRNOTAVAIL,
499
+ Errno::EHOSTUNREACH,
500
+ TLSError => e
501
+ # connect errors, exit gracefully
502
+ handle_error(e)
503
+ @state = :closed
504
+ emit(:close)
501
505
  end
502
506
 
503
507
  def handle_transition(nextstate)
@@ -54,6 +54,22 @@ module HTTPX
54
54
  Numeric.__send__(:include, NegMethods)
55
55
  end
56
56
 
57
+ module StringExtensions
58
+ refine String do
59
+ def delete_suffix!(suffix)
60
+ suffix = Backports.coerce_to_str(suffix)
61
+ chomp! if frozen?
62
+ len = suffix.length
63
+ if len > 0 && index(suffix, -len)
64
+ self[-len..-1] = ''
65
+ self
66
+ else
67
+ nil
68
+ end
69
+ end unless String.method_defined?(:delete_suffix!)
70
+ end
71
+ end
72
+
57
73
  module HashExtensions
58
74
  refine Hash do
59
75
  def compact
data/lib/httpx/headers.rb CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  module HTTPX
4
4
  class Headers
5
- EMPTY = [].freeze
6
-
7
5
  class << self
8
6
  def new(headers = nil)
9
7
  return headers if headers.is_a?(self)
data/lib/httpx/io/tcp.rb CHANGED
@@ -15,6 +15,7 @@ module HTTPX
15
15
 
16
16
  def initialize(origin, addresses, options)
17
17
  @state = :idle
18
+ @addresses = []
18
19
  @hostname = origin.host
19
20
  @options = Options.new(options)
20
21
  @fallback_protocol = @options.fallback_protocol
@@ -30,15 +31,29 @@ module HTTPX
30
31
  raise Error, "Given IO objects do not match the request authority" unless @io
31
32
 
32
33
  _, _, _, @ip = @io.addr
33
- @addresses ||= [@ip]
34
- @ip_index = @addresses.size - 1
34
+ @addresses << @ip
35
35
  @keep_open = true
36
36
  @state = :connected
37
37
  else
38
- @addresses = addresses.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
38
+ add_addresses(addresses)
39
39
  end
40
40
  @ip_index = @addresses.size - 1
41
- @io ||= build_socket
41
+ # @io ||= build_socket
42
+ end
43
+
44
+ def add_addresses(addrs)
45
+ return if addrs.empty?
46
+
47
+ addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
48
+
49
+ ip_index = @ip_index || (@addresses.size - 1)
50
+ if addrs.first.ipv6?
51
+ # should be the next in line
52
+ @addresses = [*@addresses[0, ip_index], *addrs, *@addresses[ip_index..-1]]
53
+ else
54
+ @addresses.unshift(*addrs)
55
+ @ip_index += addrs.size if @ip_index
56
+ end
42
57
  end
43
58
 
44
59
  def to_io
@@ -52,7 +67,7 @@ module HTTPX
52
67
  def connect
53
68
  return unless closed?
54
69
 
55
- if @io.closed?
70
+ if !@io || @io.closed?
56
71
  transition(:idle)
57
72
  @io = build_socket
58
73
  end
@@ -62,12 +77,16 @@ module HTTPX
62
77
  Errno::EHOSTUNREACH => e
63
78
  raise e if @ip_index <= 0
64
79
 
80
+ log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
65
81
  @ip_index -= 1
82
+ @io = build_socket
66
83
  retry
67
84
  rescue Errno::ETIMEDOUT => e
68
85
  raise ConnectTimeoutError.new(@options.timeout[:connect_timeout], e.message) if @ip_index <= 0
69
86
 
87
+ log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
70
88
  @ip_index -= 1
89
+ @io = build_socket
71
90
  retry
72
91
  end
73
92
 
data/lib/httpx/options.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "socket"
4
+
3
5
  module HTTPX
4
6
  class Options
5
7
  WINDOW_SIZE = 1 << 14 # 16K
@@ -9,6 +11,18 @@ module HTTPX
9
11
  KEEP_ALIVE_TIMEOUT = 20
10
12
  SETTINGS_TIMEOUT = 10
11
13
 
14
+ # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
15
+ ip_address_families = begin
16
+ list = Socket.ip_address_list
17
+ if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
18
+ [Socket::AF_INET6, Socket::AF_INET]
19
+ else
20
+ [Socket::AF_INET]
21
+ end
22
+ rescue NotImplementedError
23
+ [Socket::AF_INET]
24
+ end
25
+
12
26
  DEFAULT_OPTIONS = {
13
27
  :debug => ENV.key?("HTTPX_DEBUG") ? $stderr : nil,
14
28
  :debug_level => (ENV["HTTPX_DEBUG"] || 1).to_i,
@@ -37,6 +51,7 @@ module HTTPX
37
51
  :persistent => false,
38
52
  :resolver_class => (ENV["HTTPX_RESOLVER"] || :native).to_sym,
39
53
  :resolver_options => { cache: true },
54
+ :ip_families => ip_address_families,
40
55
  }.freeze
41
56
 
42
57
  begin
@@ -110,20 +125,18 @@ module HTTPX
110
125
  end
111
126
 
112
127
  def initialize(options = {})
113
- defaults = DEFAULT_OPTIONS.merge(options)
114
- defaults.each do |k, v|
115
- next if v.nil?
116
-
117
- begin
118
- value = __send__(:"option_#{k}", v)
119
- instance_variable_set(:"@#{k}", value)
120
- rescue NoMethodError
121
- raise Error, "unknown option: #{k}"
122
- end
123
- end
128
+ __initialize__(options)
124
129
  freeze
125
130
  end
126
131
 
132
+ def freeze
133
+ super
134
+ @origin.freeze
135
+ @timeout.freeze
136
+ @headers.freeze
137
+ @addresses.freeze
138
+ end
139
+
127
140
  def option_origin(value)
128
141
  URI(value)
129
142
  end
@@ -174,6 +187,10 @@ module HTTPX
174
187
  Array(value)
175
188
  end
176
189
 
190
+ def option_ip_families(value)
191
+ Array(value)
192
+ end
193
+
177
194
  %i[
178
195
  params form json body ssl http2_settings
179
196
  request_class response_class headers_class request_body_class
@@ -249,5 +266,21 @@ module HTTPX
249
266
  end
250
267
  end
251
268
  end
269
+
270
+ private
271
+
272
+ def __initialize__(options = {})
273
+ defaults = DEFAULT_OPTIONS.merge(options)
274
+ defaults.each do |k, v|
275
+ next if v.nil?
276
+
277
+ begin
278
+ value = __send__(:"option_#{k}", v)
279
+ instance_variable_set(:"@#{k}", value)
280
+ rescue NoMethodError
281
+ raise Error, "unknown option: #{k}"
282
+ end
283
+ end
284
+ end
252
285
  end
253
286
  end
@@ -18,12 +18,6 @@ module HTTPX
18
18
  require "httpx/plugins/cookies/set_cookie_parser"
19
19
  end
20
20
 
21
- module OptionsMethods
22
- def option_cookies(value)
23
- value.is_a?(Jar) ? value : Jar.new(value)
24
- end
25
- end
26
-
27
21
  module InstanceMethods
28
22
  extend Forwardable
29
23
 
@@ -77,7 +71,7 @@ module HTTPX
77
71
  end
78
72
 
79
73
  module OptionsMethods
80
- def initialize(*)
74
+ def __initialize__(*)
81
75
  super
82
76
 
83
77
  return unless @headers.key?("cookie")
@@ -89,6 +83,10 @@ module HTTPX
89
83
  end
90
84
  end
91
85
  end
86
+
87
+ def option_cookies(value)
88
+ value.is_a?(Jar) ? value : Jar.new(value)
89
+ end
92
90
  end
93
91
  end
94
92
  register_plugin :cookies, Cookies
@@ -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
 
@@ -117,6 +113,7 @@ module HTTPX
117
113
 
118
114
  def fetch_response(request, connections, options)
119
115
  response = super
116
+
120
117
  if response.is_a?(ErrorResponse) &&
121
118
  __proxy_error?(response) && !@_proxy_uris.empty?
122
119
  @_proxy_uris.shift
@@ -251,7 +248,7 @@ module HTTPX
251
248
  case nextstate
252
249
  when :closing
253
250
  # this is a hack so that we can use the super method
254
- # and it'll thing that the current state is open
251
+ # and it'll think that the current state is open
255
252
  @state = :open if @state == :connecting
256
253
  end
257
254
  super
@@ -41,7 +41,7 @@ module HTTPX
41
41
  def option_retry_after(value)
42
42
  # return early if callable
43
43
  unless value.respond_to?(:call)
44
- value = Integer(value)
44
+ value = Float(value)
45
45
  raise TypeError, ":retry_after must be positive" unless value.positive?
46
46
  end
47
47
 
@@ -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