httpx 1.5.1 → 1.6.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_6_0.md +50 -0
  3. data/doc/release_notes/1_6_1.md +17 -0
  4. data/lib/httpx/adapters/datadog.rb +23 -13
  5. data/lib/httpx/adapters/faraday.rb +14 -9
  6. data/lib/httpx/adapters/webmock.rb +1 -1
  7. data/lib/httpx/callbacks.rb +1 -1
  8. data/lib/httpx/connection/http1.rb +5 -6
  9. data/lib/httpx/connection/http2.rb +34 -18
  10. data/lib/httpx/connection.rb +19 -26
  11. data/lib/httpx/errors.rb +3 -1
  12. data/lib/httpx/io/ssl.rb +1 -4
  13. data/lib/httpx/io/tcp.rb +52 -21
  14. data/lib/httpx/io/unix.rb +4 -3
  15. data/lib/httpx/loggable.rb +4 -1
  16. data/lib/httpx/options.rb +248 -160
  17. data/lib/httpx/plugins/aws_sdk_authentication.rb +2 -0
  18. data/lib/httpx/plugins/aws_sigv4.rb +2 -0
  19. data/lib/httpx/plugins/callbacks.rb +13 -1
  20. data/lib/httpx/plugins/circuit_breaker.rb +2 -0
  21. data/lib/httpx/plugins/content_digest.rb +2 -0
  22. data/lib/httpx/plugins/cookies.rb +2 -2
  23. data/lib/httpx/plugins/digest_auth.rb +2 -0
  24. data/lib/httpx/plugins/expect.rb +2 -0
  25. data/lib/httpx/plugins/fiber_concurrency.rb +195 -0
  26. data/lib/httpx/plugins/follow_redirects.rb +2 -0
  27. data/lib/httpx/plugins/grpc.rb +2 -0
  28. data/lib/httpx/plugins/h2c.rb +26 -16
  29. data/lib/httpx/plugins/internal_telemetry.rb +0 -49
  30. data/lib/httpx/plugins/ntlm_auth.rb +2 -0
  31. data/lib/httpx/plugins/oauth.rb +10 -2
  32. data/lib/httpx/plugins/persistent.rb +27 -18
  33. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  34. data/lib/httpx/plugins/proxy/socks5.rb +1 -1
  35. data/lib/httpx/plugins/proxy/ssh.rb +2 -0
  36. data/lib/httpx/plugins/proxy.rb +61 -20
  37. data/lib/httpx/plugins/response_cache/file_store.rb +2 -2
  38. data/lib/httpx/plugins/response_cache.rb +2 -0
  39. data/lib/httpx/plugins/retries.rb +2 -0
  40. data/lib/httpx/plugins/ssrf_filter.rb +2 -2
  41. data/lib/httpx/plugins/stream_bidi.rb +3 -3
  42. data/lib/httpx/plugins/upgrade/h2.rb +11 -1
  43. data/lib/httpx/plugins/upgrade.rb +8 -0
  44. data/lib/httpx/pool.rb +15 -10
  45. data/lib/httpx/request/body.rb +8 -3
  46. data/lib/httpx/request.rb +22 -11
  47. data/lib/httpx/resolver/entry.rb +30 -0
  48. data/lib/httpx/resolver/https.rb +3 -1
  49. data/lib/httpx/resolver/multi.rb +16 -3
  50. data/lib/httpx/resolver/native.rb +15 -6
  51. data/lib/httpx/resolver/resolver.rb +15 -11
  52. data/lib/httpx/resolver/system.rb +5 -3
  53. data/lib/httpx/resolver.rb +49 -21
  54. data/lib/httpx/response/body.rb +1 -1
  55. data/lib/httpx/response/buffer.rb +13 -18
  56. data/lib/httpx/selector.rb +92 -34
  57. data/lib/httpx/session.rb +89 -30
  58. data/lib/httpx/session_extensions.rb +3 -2
  59. data/lib/httpx/transcoder/form.rb +1 -13
  60. data/lib/httpx/transcoder/multipart/mime_type_detector.rb +1 -1
  61. data/lib/httpx/transcoder/multipart.rb +14 -0
  62. data/lib/httpx/transcoder/utils/deflater.rb +1 -1
  63. data/lib/httpx/version.rb +1 -1
  64. data/sig/callbacks.rbs +1 -1
  65. data/sig/chainable.rbs +1 -0
  66. data/sig/connection/http1.rbs +2 -0
  67. data/sig/connection/http2.rbs +5 -1
  68. data/sig/connection.rbs +6 -6
  69. data/sig/errors.rbs +3 -1
  70. data/sig/io/ssl.rbs +1 -1
  71. data/sig/io/tcp.rbs +13 -7
  72. data/sig/io/udp.rbs +7 -2
  73. data/sig/io/unix.rbs +0 -1
  74. data/sig/io.rbs +0 -3
  75. data/sig/options.rbs +64 -11
  76. data/sig/plugins/fiber_concurrency.rbs +51 -0
  77. data/sig/plugins/h2c.rbs +5 -1
  78. data/sig/plugins/oauth.rbs +15 -1
  79. data/sig/plugins/persistent.rbs +1 -1
  80. data/sig/plugins/proxy/socks4.rbs +1 -1
  81. data/sig/plugins/proxy/socks5.rbs +1 -1
  82. data/sig/plugins/proxy.rbs +5 -2
  83. data/sig/plugins/ssrf_filter.rbs +1 -1
  84. data/sig/plugins/stream_bidi.rbs +2 -2
  85. data/sig/request.rbs +4 -1
  86. data/sig/resolver/entry.rbs +13 -0
  87. data/sig/resolver/native.rbs +1 -0
  88. data/sig/resolver/resolver.rbs +2 -3
  89. data/sig/resolver/system.rbs +2 -2
  90. data/sig/resolver.rbs +12 -11
  91. data/sig/response.rbs +2 -2
  92. data/sig/selector.rbs +18 -10
  93. data/sig/session.rbs +4 -0
  94. data/sig/transcoder/form.rbs +3 -3
  95. data/sig/transcoder/multipart.rbs +9 -3
  96. metadata +11 -3
@@ -57,6 +57,8 @@ module HTTPX
57
57
  # :retry_on :: callable which alternatively defines a different rule for when a response is to be retried
58
58
  # (i.e. <tt>->(res) { ... }</tt>).
59
59
  module OptionsMethods
60
+ private
61
+
60
62
  def option_retry_after(value)
61
63
  # return early if callable
62
64
  unless value.respond_to?(:call)
@@ -91,6 +91,8 @@ module HTTPX
91
91
  #
92
92
  # :allowed_schemes :: list of URI schemes allowed (defaults to <tt>["https", "http"]</tt>)
93
93
  module OptionsMethods
94
+ private
95
+
94
96
  def option_allowed_schemes(value)
95
97
  Array(value)
96
98
  end
@@ -129,8 +131,6 @@ module HTTPX
129
131
  end
130
132
 
131
133
  def addresses=(addrs)
132
- addrs = addrs.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
133
-
134
134
  addrs.reject!(&SsrfFilter.method(:unsafe_ip_address?))
135
135
 
136
136
  raise ServerSideRequestForgeryError, "#{@origin.host} has no public IP addresses" if addrs.empty?
@@ -42,7 +42,7 @@ module HTTPX
42
42
  %i[join_headers join_trailers join_body].each do |lock_meth|
43
43
  class_eval(<<-METH, __FILE__, __LINE__ + 1)
44
44
  # lock.aware version of +#{lock_meth}+
45
- def #{lock_meth}(*) # def join_headers(*)
45
+ private def #{lock_meth}(*) # private def join_headers(*)
46
46
  return super if @lock.owned?
47
47
 
48
48
  # small race condition between
@@ -119,7 +119,7 @@ module HTTPX
119
119
  class Signal
120
120
  def initialize
121
121
  @closed = false
122
- @pipe_read, @pipe_write = ::IO.pipe
122
+ @pipe_read, @pipe_write = IO.pipe
123
123
  end
124
124
 
125
125
  def state
@@ -127,7 +127,7 @@ module HTTPX
127
127
  end
128
128
 
129
129
  # noop
130
- def log(**); end
130
+ def log(**, &_); end
131
131
 
132
132
  def to_io
133
133
  @pipe_read.to_io
@@ -22,6 +22,16 @@ module HTTPX
22
22
  module ConnectionMethods
23
23
  using URIExtensions
24
24
 
25
+ def interests
26
+ return super unless connecting? && @parser
27
+
28
+ connect
29
+
30
+ return @io.interests if connecting?
31
+
32
+ super
33
+ end
34
+
25
35
  def upgrade_to_h2
26
36
  prev_parser = @parser
27
37
 
@@ -30,7 +40,7 @@ module HTTPX
30
40
  @inflight -= prev_parser.requests.size
31
41
  end
32
42
 
33
- @parser = Connection::HTTP2.new(@write_buffer, @options)
43
+ @parser = @options.http2_class.new(@write_buffer, @options)
34
44
  set_parser_callbacks(@parser)
35
45
  @upgrade_protocol = "h2"
36
46
 
@@ -20,6 +20,8 @@ module HTTPX
20
20
  end
21
21
 
22
22
  module OptionsMethods
23
+ private
24
+
23
25
  def option_upgrade_handlers(value)
24
26
  raise TypeError, ":upgrade_handlers must be a Hash" unless value.is_a?(Hash)
25
27
 
@@ -65,6 +67,12 @@ module HTTPX
65
67
  module ConnectionMethods
66
68
  attr_reader :upgrade_protocol, :hijacked
67
69
 
70
+ def initialize(*)
71
+ super
72
+
73
+ @upgrade_protocol = nil
74
+ end
75
+
68
76
  def hijack_io
69
77
  @hijacked = true
70
78
 
data/lib/httpx/pool.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "httpx/selector"
4
4
  require "httpx/connection"
5
+ require "httpx/connection/http2"
6
+ require "httpx/connection/http1"
5
7
  require "httpx/resolver"
6
8
 
7
9
  module HTTPX
@@ -51,18 +53,21 @@ module HTTPX
51
53
  # this takes precedence over per-origin
52
54
  @max_connections_cond.wait(@connection_mtx, @pool_timeout)
53
55
 
54
- acquire_connection(uri, options) || begin
55
- if @connections_counter == @max_connections
56
- # if no matching usable connection was found, the pool will make room and drop a closed connection. if none is found,
57
- # this means that all of them are persistent or being used, so raise a timeout error.
58
- conn = @connections.find { |c| c.state == :closed }
56
+ if (conn = acquire_connection(uri, options))
57
+ return conn
58
+ end
59
+
60
+ if @connections_counter == @max_connections
61
+ # if no matching usable connection was found, the pool will make room and drop a closed connection. if none is found,
62
+ # this means that all of them are persistent or being used, so raise a timeout error.
63
+ conn = @connections.find { |c| c.state == :closed }
59
64
 
60
- raise PoolTimeoutError.new(@pool_timeout,
61
- "Timed out after #{@pool_timeout} seconds while waiting for a connection") unless conn
65
+ raise PoolTimeoutError.new(@pool_timeout,
66
+ "Timed out after #{@pool_timeout} seconds while waiting for a connection") unless conn
62
67
 
63
- drop_connection(conn)
64
- end
68
+ drop_connection(conn)
65
69
  end
70
+
66
71
  end
67
72
 
68
73
  if @origin_counters[uri.origin] == @max_connections_per_origin
@@ -110,7 +115,7 @@ module HTTPX
110
115
 
111
116
  def checkout_resolver(options)
112
117
  resolver_type = options.resolver_class
113
- resolver_type = Resolver.resolver_for(resolver_type)
118
+ resolver_type = Resolver.resolver_for(resolver_type, options)
114
119
 
115
120
  @resolver_mtx.synchronize do
116
121
  resolvers = @resolvers[resolver_type]
@@ -56,7 +56,7 @@ module HTTPX
56
56
  block.call(chunk)
57
57
  end
58
58
  # TODO: use copy_stream once bug is resolved: https://bugs.ruby-lang.org/issues/21131
59
- # ::IO.copy_stream(body, ProcIO.new(block))
59
+ # IO.copy_stream(body, ProcIO.new(block))
60
60
  elsif body.respond_to?(:each)
61
61
  body.each(&block)
62
62
  else
@@ -127,8 +127,13 @@ module HTTPX
127
127
  # @type var body: bodyIO
128
128
  Transcoder::Body.encode(body)
129
129
  elsif (form = params.delete(:form))
130
- # @type var form: Transcoder::urlencoded_input
131
- Transcoder::Form.encode(form)
130
+ if Transcoder::Multipart.multipart?(form)
131
+ # @type var form: Transcoder::multipart_input
132
+ Transcoder::Multipart.encode(form)
133
+ else
134
+ # @type var form: Transcoder::urlencoded_input
135
+ Transcoder::Form.encode(form)
136
+ end
132
137
  elsif (json = params.delete(:json))
133
138
  # @type var body: _ToJson
134
139
  Transcoder::JSON.encode(json)
data/lib/httpx/request.rb CHANGED
@@ -8,6 +8,7 @@ module HTTPX
8
8
  # as well as maintaining the state machine which manages streaming the request onto the wire.
9
9
  class Request
10
10
  extend Forwardable
11
+ include Loggable
11
12
  include Callbacks
12
13
  using URIExtensions
13
14
 
@@ -102,13 +103,16 @@ module HTTPX
102
103
  raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
103
104
 
104
105
  @state = :idle
105
- @response = nil
106
- @peer_address = nil
106
+ @response = @peer_address = @context = @informational_status = nil
107
107
  @ping = false
108
108
  @persistent = @options.persistent
109
109
  @active_timeouts = []
110
110
  end
111
111
 
112
+ def complete!(response = @response)
113
+ emit(:complete, response)
114
+ end
115
+
112
116
  # whether request has been buffered with a ping
113
117
  def ping?
114
118
  @ping
@@ -173,17 +177,23 @@ module HTTPX
173
177
  def response=(response)
174
178
  return unless response
175
179
 
176
- if response.is_a?(Response) && response.status < 200
177
- # deal with informational responses
180
+ case response
181
+ when Response
182
+ if response.status < 200
183
+ # deal with informational responses
178
184
 
179
- if response.status == 100 && @headers.key?("expect")
180
- @informational_status = response.status
181
- return
182
- end
185
+ if response.status == 100 && @headers.key?("expect")
186
+ @informational_status = response.status
187
+ return
188
+ end
189
+
190
+ # 103 Early Hints advertises resources in document to browsers.
191
+ # not very relevant for an HTTP client, discard.
192
+ return if response.status >= 103
183
193
 
184
- # 103 Early Hints advertises resources in document to browsers.
185
- # not very relevant for an HTTP client, discard.
186
- return if response.status >= 103
194
+ end
195
+ when ErrorResponse
196
+ response.error.connection = nil if response.error.respond_to?(:connection=)
187
197
  end
188
198
 
189
199
  @response = response
@@ -293,6 +303,7 @@ module HTTPX
293
303
  return if @state == :expect
294
304
 
295
305
  end
306
+ log(level: 3) { "#{@state}] -> #{nextstate}" }
296
307
  @state = nextstate
297
308
  emit(@state, self)
298
309
  nil
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module HTTPX
6
+ module Resolver
7
+ class Entry < SimpleDelegator
8
+ attr_reader :address
9
+
10
+ def self.convert(address)
11
+ new(address, rescue_on_convert: true)
12
+ end
13
+
14
+ def initialize(address, expires_in = Float::INFINITY, rescue_on_convert: false)
15
+ @expires_in = expires_in
16
+ @address = address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
17
+ super(@address)
18
+ rescue IPAddr::InvalidAddressError
19
+ raise unless rescue_on_convert
20
+
21
+ @address = address.to_s
22
+ super(@address)
23
+ end
24
+
25
+ def expired?
26
+ @expires_in < Utils.now
27
+ end
28
+ end
29
+ end
30
+ end
@@ -59,6 +59,8 @@ module HTTPX
59
59
  resolve(connection)
60
60
  end
61
61
 
62
+ # This is already indirectly monitored bt the HTTP connection. In order to skip
63
+ # monitoring, this method returns <tt>true</tt>.
62
64
  def closed?
63
65
  true
64
66
  end
@@ -202,7 +204,7 @@ module HTTPX
202
204
  @queries.delete_if { |_, conn| connection == conn }
203
205
 
204
206
  Resolver.cached_lookup_set(hostname, @family, addresses) if @resolver_options[:cache]
205
- catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
207
+ catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) }) }
206
208
  end
207
209
  end
208
210
  return if @connections.empty?
@@ -11,12 +11,13 @@ module HTTPX
11
11
  attr_reader :resolvers, :options
12
12
 
13
13
  def initialize(resolver_type, options)
14
- @current_selector = nil
15
- @current_session = nil
14
+ @current_selector = @current_session = nil
16
15
  @options = options
17
16
  @resolver_options = @options.resolver_options
18
17
 
19
- @resolvers = options.ip_families.map do |ip_family|
18
+ ip_families = options.ip_families || Resolver.supported_ip_families
19
+
20
+ @resolvers = ip_families.map do |ip_family|
20
21
  resolver = resolver_type.new(ip_family, options)
21
22
  resolver.multi = self
22
23
  resolver
@@ -35,6 +36,10 @@ module HTTPX
35
36
  @resolvers.each { |r| r.__send__(__method__, s) }
36
37
  end
37
38
 
39
+ def log(*args, **kwargs, &blk)
40
+ @resolvers.each { |r| r.__send__(__method__, *args, **kwargs, &blk) }
41
+ end
42
+
38
43
  def closed?
39
44
  @resolvers.all?(&:closed?)
40
45
  end
@@ -64,8 +69,12 @@ module HTTPX
64
69
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
65
70
  return false unless addresses
66
71
 
72
+ ip_families = connection.options.ip_families || Resolver.supported_ip_families
73
+
67
74
  resolved = false
68
75
  addresses.group_by(&:family).sort { |(f1, _), (f2, _)| f2 <=> f1 }.each do |family, addrs|
76
+ next unless ip_families.include?(family)
77
+
69
78
  # try to match the resolver by family. However, there are cases where that's not possible, as when
70
79
  # the system does not have IPv6 connectivity, but it does support IPv6 via loopback/link-local.
71
80
  resolver = @resolvers.find { |r| r.family == family } || @resolvers.first
@@ -82,7 +91,11 @@ module HTTPX
82
91
  end
83
92
 
84
93
  def lazy_resolve(connection)
94
+ ip_families = connection.options.ip_families || Resolver.supported_ip_families
95
+
85
96
  @resolvers.each do |resolver|
97
+ next unless ip_families.include?(resolver.family)
98
+
86
99
  resolver << @current_session.try_clone_connection(connection, @current_selector, resolver.family)
87
100
  next if resolver.empty?
88
101
 
@@ -42,6 +42,7 @@ module HTTPX
42
42
  @read_buffer = "".b
43
43
  @write_buffer = Buffer.new(@resolver_options[:packet_size])
44
44
  @state = :idle
45
+ @timer = nil
45
46
  end
46
47
 
47
48
  def close
@@ -104,11 +105,11 @@ module HTTPX
104
105
  private
105
106
 
106
107
  def calculate_interests
107
- return :w unless @write_buffer.empty?
108
+ return if @queries.empty?
108
109
 
109
- return :r unless @queries.empty?
110
+ return :r if @write_buffer.empty?
110
111
 
111
- nil
112
+ :w
112
113
  end
113
114
 
114
115
  def consume
@@ -153,6 +154,8 @@ module HTTPX
153
154
  @timer = @current_selector.after(timeout) do
154
155
  next unless @connections.include?(connection)
155
156
 
157
+ @timer = nil
158
+
156
159
  do_retry(h, connection, timeout)
157
160
  end
158
161
  end
@@ -270,6 +273,8 @@ module HTTPX
270
273
  def parse(buffer)
271
274
  @timer.cancel
272
275
 
276
+ @timer = nil
277
+
273
278
  code, result = Resolver.decode_dns_answer(buffer)
274
279
 
275
280
  case code
@@ -370,7 +375,9 @@ module HTTPX
370
375
  @timeouts.delete(connection.peer.host)
371
376
  @connections.delete(connection)
372
377
  Resolver.cached_lookup_set(connection.peer.host, @family, addresses) if @resolver_options[:cache]
373
- catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
378
+ catch(:coalesced) do
379
+ emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) })
380
+ end
374
381
  end
375
382
  end
376
383
  close_or_resolve
@@ -383,7 +390,8 @@ module HTTPX
383
390
 
384
391
  raise Error, "no URI to resolve" unless connection
385
392
 
386
- return unless @write_buffer.empty?
393
+ # do not buffer query if previous is still in the buffer or awaiting reply/retry
394
+ return unless @write_buffer.empty? && @timer.nil?
387
395
 
388
396
  hostname ||= @queries.key(connection)
389
397
 
@@ -444,7 +452,7 @@ module HTTPX
444
452
  when :tcp
445
453
  log { "resolver #{FAMILY_TYPES[@record_type]}: server: tcp://#{ip}:#{port}..." }
446
454
  origin = URI("tcp://#{ip}:#{port}")
447
- TCP.new(origin, [ip], @options)
455
+ TCP.new(origin, [Resolver::Entry.new(ip)], @options)
448
456
  end
449
457
  end
450
458
 
@@ -480,6 +488,7 @@ module HTTPX
480
488
  @write_buffer.clear
481
489
  @read_buffer.clear
482
490
  end
491
+ log(level: 3) { "#{@state} -> #{nextstate}" }
483
492
  @state = nextstate
484
493
  rescue Errno::ECONNREFUSED,
485
494
  Errno::EADDRNOTAVAIL,
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "resolv"
4
- require "ipaddr"
5
4
 
6
5
  module HTTPX
7
6
  # Base class for all internal internet name resolvers. It handles basic blocks
@@ -69,24 +68,29 @@ module HTTPX
69
68
  end
70
69
 
71
70
  def emit_addresses(connection, family, addresses, early_resolve = false)
72
- addresses.map! do |address|
73
- address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
74
- end
71
+ addresses.map! { |address| address.is_a?(Resolver::Entry) ? address : Resolver::Entry.new(address) }
75
72
 
76
73
  # double emission check, but allow early resolution to work
77
- return if !early_resolve && connection.addresses && !addresses.intersect?(connection.addresses)
74
+ conn_addrs = connection.addresses
75
+ return if !early_resolve && conn_addrs && (!conn_addrs.empty? && !addresses.intersect?(!conn_addrs))
78
76
 
79
77
  log do
80
78
  "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
81
79
  "answer #{connection.peer.host}: #{addresses.inspect} (early resolve: #{early_resolve})"
82
80
  end
83
81
 
84
- if !early_resolve && # do not apply resolution delay for non-dns name resolution
85
- @current_selector && # just in case...
86
- family == Socket::AF_INET && # resolution delay only applies to IPv4
87
- !connection.io && # connection already has addresses and initiated/ended handshake
88
- connection.options.ip_families.size > 1 && # no need to delay if not supporting dual stack IP
89
- addresses.first.to_s != connection.peer.host.to_s # connection URL host is already the IP (early resolve included perhaps?)
82
+ # do not apply resolution delay for non-dns name resolution
83
+ if !early_resolve &&
84
+ # just in case...
85
+ @current_selector &&
86
+ # resolution delay only applies to IPv4
87
+ family == Socket::AF_INET &&
88
+ # connection already has addresses and initiated/ended handshake
89
+ !connection.io &&
90
+ # no need to delay if not supporting dual stack / multi-homed IP
91
+ (connection.options.ip_families || Resolver.supported_ip_families).size > 1 &&
92
+ # connection URL host is already the IP (early resolve included perhaps?)
93
+ addresses.first.to_s != connection.peer.host.to_s
90
94
  log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }
91
95
 
92
96
  @current_selector.after(0.05) do
@@ -127,7 +127,7 @@ module HTTPX
127
127
  when :open
128
128
  return unless @state == :idle
129
129
 
130
- @pipe_read, @pipe_write = ::IO.pipe
130
+ @pipe_read, @pipe_write = IO.pipe
131
131
  when :closed
132
132
  return unless @state == :open
133
133
 
@@ -187,7 +187,9 @@ module HTTPX
187
187
 
188
188
  transition(:open)
189
189
 
190
- connection.options.ip_families.each do |family|
190
+ ip_families = connection.options.ip_families || Resolver.supported_ip_families
191
+
192
+ ip_families.each do |family|
191
193
  @queries << [family, connection]
192
194
  end
193
195
  async_resolve(connection, hostname, scheme)
@@ -195,7 +197,7 @@ module HTTPX
195
197
  end
196
198
 
197
199
  def async_resolve(connection, hostname, scheme)
198
- families = connection.options.ip_families
200
+ families = connection.options.ip_families || Resolver.supported_ip_families
199
201
  log { "resolver: query for #{hostname}" }
200
202
  timeouts = @timeouts[connection.peer.host]
201
203
  resolve_timeout = timeouts.first
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "socket"
3
4
  require "resolv"
4
- require "ipaddr"
5
5
 
6
6
  module HTTPX
7
7
  module Resolver
8
8
  RESOLVE_TIMEOUT = [2, 3].freeze
9
9
 
10
+ require "httpx/resolver/entry"
10
11
  require "httpx/resolver/resolver"
11
12
  require "httpx/resolver/system"
12
13
  require "httpx/resolver/native"
@@ -18,36 +19,54 @@ module HTTPX
18
19
 
19
20
  @identifier_mutex = Thread::Mutex.new
20
21
  @identifier = 1
21
- @system_resolver = Resolv::Hosts.new
22
+ @hosts_resolver = Resolv::Hosts.new
22
23
 
23
24
  module_function
24
25
 
25
- def resolver_for(resolver_type)
26
+ def supported_ip_families
27
+ @supported_ip_families ||= begin
28
+ # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
29
+ list = Socket.ip_address_list
30
+ if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
31
+ [Socket::AF_INET6, Socket::AF_INET]
32
+ else
33
+ [Socket::AF_INET]
34
+ end
35
+ rescue NotImplementedError
36
+ [Socket::AF_INET]
37
+ end.freeze
38
+ end
39
+
40
+ def resolver_for(resolver_type, options)
26
41
  case resolver_type
27
- when :native then Native
28
- when :system then System
29
- when :https then HTTPS
30
- else
31
- return resolver_type if resolver_type.is_a?(Class) && resolver_type < Resolver
42
+ when Symbol
43
+ meth = :"resolver_#{resolver_type}_class"
32
44
 
33
- raise Error, "unsupported resolver type (#{resolver_type})"
45
+ return options.__send__(meth) if options.respond_to?(meth)
46
+ when Class
47
+ return resolver_type if resolver_type < Resolver
34
48
  end
49
+
50
+ raise Error, "unsupported resolver type (#{resolver_type})"
35
51
  end
36
52
 
37
53
  def nolookup_resolve(hostname)
38
- ip_resolve(hostname) || cached_lookup(hostname) || system_resolve(hostname)
54
+ ip_resolve(hostname) || cached_lookup(hostname) || hosts_resolve(hostname)
39
55
  end
40
56
 
57
+ # tries to convert +hostname+ into an IPAddr, returns <tt>nil</tt> otherwise.
41
58
  def ip_resolve(hostname)
42
- [IPAddr.new(hostname)]
59
+ [Entry.new(hostname)]
43
60
  rescue ArgumentError
44
61
  end
45
62
 
46
- def system_resolve(hostname)
47
- ips = @system_resolver.getaddresses(hostname)
63
+ # matches +hostname+ to entries in the hosts file, returns <tt>nil</nil> if none is
64
+ # found, or there is no hosts file.
65
+ def hosts_resolve(hostname)
66
+ ips = @hosts_resolver.getaddresses(hostname)
48
67
  return if ips.empty?
49
68
 
50
- ips.map { |ip| IPAddr.new(ip) }
69
+ ips.map { |ip| Entry.new(ip) }
51
70
  rescue IOError
52
71
  end
53
72
 
@@ -59,10 +78,6 @@ module HTTPX
59
78
  end
60
79
 
61
80
  def cached_lookup_set(hostname, family, entries)
62
- now = Utils.now
63
- entries.each do |entry|
64
- entry["TTL"] += now
65
- end
66
81
  lookup_synchronize do |lookups|
67
82
  case family
68
83
  when Socket::AF_INET6
@@ -83,6 +98,18 @@ module HTTPX
83
98
  end
84
99
  end
85
100
 
101
+ def cached_lookup_evict(hostname, ip)
102
+ ip = ip.to_s
103
+
104
+ lookup_synchronize do |lookups|
105
+ entries = lookups[hostname]
106
+
107
+ return unless entries
108
+
109
+ lookups.delete_if { |entry| entry["data"] == ip }
110
+ end
111
+ end
112
+
86
113
  # do not use directly!
87
114
  def lookup(hostname, lookups, ttl)
88
115
  return unless lookups.key?(hostname)
@@ -95,7 +122,7 @@ module HTTPX
95
122
  if (als = address["alias"])
96
123
  lookup(als, lookups, ttl)
97
124
  else
98
- IPAddr.new(address["data"])
125
+ Entry.new(address["data"], address["TTL"])
99
126
  end
100
127
  end.compact
101
128
 
@@ -129,19 +156,20 @@ module HTTPX
129
156
 
130
157
  addresses = []
131
158
 
159
+ now = Utils.now
132
160
  message.each_answer do |question, _, value|
133
161
  case value
134
162
  when Resolv::DNS::Resource::IN::CNAME
135
163
  addresses << {
136
164
  "name" => question.to_s,
137
- "TTL" => value.ttl,
165
+ "TTL" => (now + value.ttl),
138
166
  "alias" => value.name.to_s,
139
167
  }
140
168
  when Resolv::DNS::Resource::IN::A,
141
169
  Resolv::DNS::Resource::IN::AAAA
142
170
  addresses << {
143
171
  "name" => question.to_s,
144
- "TTL" => value.ttl,
172
+ "TTL" => (now + value.ttl),
145
173
  "data" => value.address.to_s,
146
174
  }
147
175
  end
@@ -136,7 +136,7 @@ module HTTPX
136
136
  if dest.respond_to?(:path) && @buffer.respond_to?(:path)
137
137
  FileUtils.mv(@buffer.path, dest.path)
138
138
  else
139
- ::IO.copy_stream(@buffer, dest)
139
+ IO.copy_stream(@buffer, dest)
140
140
  end
141
141
  end
142
142