httpx 1.6.3 → 1.7.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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_11_0.md +3 -3
  3. data/doc/release_notes/1_6_3.md +2 -2
  4. data/doc/release_notes/1_7_0.md +149 -0
  5. data/doc/release_notes/1_7_1.md +21 -0
  6. data/lib/httpx/adapters/datadog.rb +1 -1
  7. data/lib/httpx/adapters/faraday.rb +1 -1
  8. data/lib/httpx/adapters/webmock.rb +18 -9
  9. data/lib/httpx/altsvc.rb +4 -2
  10. data/lib/httpx/connection/http1.rb +9 -9
  11. data/lib/httpx/connection/http2.rb +2 -0
  12. data/lib/httpx/connection.rb +7 -9
  13. data/lib/httpx/domain_name.rb +1 -1
  14. data/lib/httpx/headers.rb +2 -2
  15. data/lib/httpx/io/tcp.rb +1 -1
  16. data/lib/httpx/loggable.rb +2 -0
  17. data/lib/httpx/options.rb +118 -22
  18. data/lib/httpx/parser/http1.rb +1 -0
  19. data/lib/httpx/plugins/auth/digest.rb +44 -4
  20. data/lib/httpx/plugins/auth.rb +113 -4
  21. data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
  22. data/lib/httpx/plugins/cookies/cookie.rb +1 -0
  23. data/lib/httpx/plugins/digest_auth.rb +4 -5
  24. data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
  25. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
  26. data/lib/httpx/plugins/grpc.rb +2 -2
  27. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  28. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  29. data/lib/httpx/plugins/oauth.rb +156 -57
  30. data/lib/httpx/plugins/persistent.rb +3 -5
  31. data/lib/httpx/plugins/proxy/http.rb +0 -4
  32. data/lib/httpx/plugins/proxy.rb +3 -1
  33. data/lib/httpx/plugins/query.rb +1 -1
  34. data/lib/httpx/plugins/rate_limiter.rb +20 -15
  35. data/lib/httpx/plugins/response_cache.rb +3 -7
  36. data/lib/httpx/plugins/retries.rb +60 -24
  37. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  38. data/lib/httpx/plugins/stream.rb +60 -9
  39. data/lib/httpx/plugins/stream_bidi.rb +84 -16
  40. data/lib/httpx/pool.rb +12 -3
  41. data/lib/httpx/request/body.rb +1 -1
  42. data/lib/httpx/request.rb +10 -1
  43. data/lib/httpx/resolver/cache/base.rb +136 -0
  44. data/lib/httpx/resolver/cache/memory.rb +42 -0
  45. data/lib/httpx/resolver/cache.rb +18 -0
  46. data/lib/httpx/resolver/https.rb +74 -20
  47. data/lib/httpx/resolver/multi.rb +10 -2
  48. data/lib/httpx/resolver/native.rb +32 -6
  49. data/lib/httpx/resolver/resolver.rb +3 -3
  50. data/lib/httpx/resolver.rb +36 -114
  51. data/lib/httpx/response/body.rb +5 -3
  52. data/lib/httpx/response.rb +22 -6
  53. data/lib/httpx/selector.rb +14 -3
  54. data/lib/httpx/session.rb +6 -6
  55. data/lib/httpx/timers.rb +6 -12
  56. data/lib/httpx/transcoder/body.rb +1 -1
  57. data/lib/httpx/transcoder/gzip.rb +7 -2
  58. data/lib/httpx/transcoder/json.rb +1 -1
  59. data/lib/httpx/transcoder/multipart/decoder.rb +5 -5
  60. data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
  61. data/lib/httpx/transcoder/multipart.rb +17 -9
  62. data/lib/httpx/transcoder.rb +4 -6
  63. data/lib/httpx/utils.rb +13 -0
  64. data/lib/httpx/version.rb +1 -1
  65. data/sig/altsvc.rbs +9 -3
  66. data/sig/chainable.rbs +3 -3
  67. data/sig/connection.rbs +1 -3
  68. data/sig/loggable.rbs +1 -1
  69. data/sig/options.rbs +12 -4
  70. data/sig/plugins/auth/digest.rbs +6 -0
  71. data/sig/plugins/auth.rbs +37 -4
  72. data/sig/plugins/basic_auth.rbs +3 -3
  73. data/sig/plugins/digest_auth.rbs +2 -4
  74. data/sig/plugins/fiber_concurrency.rbs +6 -0
  75. data/sig/plugins/ntlm_auth.rbs +2 -2
  76. data/sig/plugins/oauth.rbs +44 -15
  77. data/sig/plugins/rate_limiter.rbs +4 -2
  78. data/sig/plugins/response_cache/file_store.rbs +2 -0
  79. data/sig/plugins/response_cache.rbs +4 -0
  80. data/sig/plugins/retries.rbs +12 -4
  81. data/sig/plugins/stream.rbs +13 -3
  82. data/sig/plugins/stream_bidi.rbs +2 -2
  83. data/sig/pool.rbs +1 -1
  84. data/sig/resolver/cache/base.rbs +28 -0
  85. data/sig/resolver/cache/memory.rbs +13 -0
  86. data/sig/resolver/cache.rbs +16 -0
  87. data/sig/resolver/https.rbs +24 -0
  88. data/sig/resolver/multi.rbs +8 -0
  89. data/sig/resolver/native.rbs +2 -0
  90. data/sig/resolver.rbs +5 -20
  91. data/sig/response.rbs +3 -0
  92. data/sig/session.rbs +3 -5
  93. data/sig/timers.rbs +1 -1
  94. data/sig/transcoder/multipart.rbs +4 -2
  95. data/sig/transcoder.rbs +5 -1
  96. data/sig/utils.rbs +2 -0
  97. metadata +11 -1
@@ -12,6 +12,7 @@ module HTTPX
12
12
  #
13
13
  class Resolver::HTTPS < Resolver::Resolver
14
14
  extend Forwardable
15
+
15
16
  using URIExtensions
16
17
 
17
18
  module DNSExtensions
@@ -30,7 +31,7 @@ module HTTPX
30
31
  use_get: false,
31
32
  }.freeze
32
33
 
33
- def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close,
34
+ def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close,
34
35
  :closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout
35
36
 
36
37
  def initialize(_, options)
@@ -38,17 +39,23 @@ module HTTPX
38
39
  @resolver_options = DEFAULTS.merge(@options.resolver_options)
39
40
  @queries = {}
40
41
  @requests = {}
42
+ @_timeouts = Array(@resolver_options[:timeouts])
43
+ @timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
41
44
  @uri = URI(@resolver_options[:uri])
42
- @uri_addresses = nil
45
+ @name = @uri_addresses = nil
43
46
  @resolver = Resolv::DNS.new
44
- @resolver.timeouts = @resolver_options.fetch(:timeouts, Resolver::RESOLVE_TIMEOUT)
47
+ @resolver.timeouts = @_timeouts.empty? ? Resolver::RESOLVE_TIMEOUT : @_timeouts
45
48
  @resolver.lazy_initialize
46
49
  end
47
50
 
51
+ def state
52
+ @resolver_connection ? @resolver_connection.state : :idle
53
+ end
54
+
48
55
  def <<(connection)
49
56
  return if @uri.origin == connection.peer.to_s
50
57
 
51
- @uri_addresses ||= HTTPX::Resolver.nolookup_resolve(@uri.host) || @resolver.getaddresses(@uri.host)
58
+ @uri_addresses ||= @options.resolver_cache.resolve(@uri.host) || @resolver.getaddresses(@uri.host)
52
59
 
53
60
  if @uri_addresses.empty?
54
61
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
@@ -96,21 +103,26 @@ module HTTPX
96
103
  else
97
104
  @queries[hostname] = connection
98
105
  end
106
+
107
+ @name = hostname
108
+
99
109
  log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
100
110
 
101
- begin
102
- request = build_request(hostname)
103
- request.on(:response, &method(:on_response).curry(2)[request])
104
- request.on(:promise, &method(:on_promise))
105
- @requests[request] = hostname
106
- resolver_connection.send(request)
107
- @connections << connection
108
- rescue ResolveError, Resolv::DNS::EncodeError => e
109
- reset_hostname(hostname)
110
- throw(:resolve_error, e) if connection.pending.empty?
111
- emit_resolve_error(connection, connection.peer.host, e)
112
- close_or_resolve
113
- end
111
+ send_request(hostname, connection)
112
+ end
113
+
114
+ def send_request(hostname, connection)
115
+ request = build_request(hostname)
116
+ request.on(:response, &method(:on_response).curry(2)[request])
117
+ request.on(:promise, &method(:on_promise))
118
+ @requests[request] = hostname
119
+ resolver_connection.send(request)
120
+ @connections << connection
121
+ rescue ResolveError, Resolv::DNS::EncodeError => e
122
+ reset_hostname(hostname)
123
+ throw(:resolve_error, e) if connection.pending.empty?
124
+ emit_resolve_error(connection, connection.peer.host, e)
125
+ close_or_resolve
114
126
  end
115
127
 
116
128
  def on_response(request, response)
@@ -122,6 +134,18 @@ module HTTPX
122
134
  close_or_resolve
123
135
  else
124
136
  # @type var response: HTTPX::Response
137
+ if response.status.between?(300, 399) && response.headers.key?("location")
138
+ hostname = @requests[request]
139
+ connection = @queries[hostname]
140
+ location_uri = URI(response.headers["location"])
141
+ location_uri = response.uri.merge(location_uri) if location_uri.relative?
142
+
143
+ # we assume that the DNS server URI changed permanently and move on
144
+ @uri = location_uri
145
+ send_request(hostname, connection)
146
+ return
147
+ end
148
+
125
149
  parse(request, response)
126
150
  ensure
127
151
  @requests.delete(request)
@@ -133,6 +157,10 @@ module HTTPX
133
157
  end
134
158
 
135
159
  def parse(request, response)
160
+ hostname = @name
161
+
162
+ @name = nil
163
+
136
164
  code, result = decode_response_body(response)
137
165
 
138
166
  case code
@@ -151,6 +179,23 @@ module HTTPX
151
179
  end
152
180
 
153
181
  resolve
182
+ when :retriable_error
183
+ timeouts = @timeouts[hostname]
184
+
185
+ unless timeouts.empty?
186
+ log { "resolver #{FAMILY_TYPES[@record_type]}: failed, but will retry..." }
187
+
188
+ connection = @queries[hostname]
189
+
190
+ resolve(connection, hostname)
191
+ return
192
+ end
193
+
194
+ host = @requests.delete(request)
195
+ connection = reset_hostname(host)
196
+
197
+ emit_resolve_error(connection)
198
+ close_or_resolve
154
199
  when :dns_error
155
200
  host = @requests.delete(request)
156
201
  connection = reset_hostname(host)
@@ -206,7 +251,7 @@ module HTTPX
206
251
  # eliminate other candidates
207
252
  @queries.delete_if { |_, conn| connection == conn }
208
253
 
209
- Resolver.cached_lookup_set(hostname, @family, addresses) if @resolver_options[:cache]
254
+ @options.resolver_cache.set(hostname, @family, addresses) if @resolver_options[:cache]
210
255
  catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) }) }
211
256
  end
212
257
  end
@@ -217,15 +262,18 @@ module HTTPX
217
262
  uri = @uri.dup
218
263
  rklass = @options.request_class
219
264
  payload = Resolver.encode_dns_query(hostname, type: @record_type)
265
+ timeouts = @timeouts[hostname]
266
+ request_timeout = timeouts.shift
267
+ options = @options.merge(timeout: { request_timeout: request_timeout })
220
268
 
221
269
  if @resolver_options[:use_get]
222
270
  params = URI.decode_www_form(uri.query.to_s)
223
271
  params << ["type", FAMILY_TYPES[@record_type]]
224
272
  params << ["dns", Base64.urlsafe_encode64(payload, padding: false)]
225
273
  uri.query = URI.encode_www_form(params)
226
- request = rklass.new("GET", uri, @options)
274
+ request = rklass.new("GET", uri, options)
227
275
  else
228
- request = rklass.new("POST", uri, @options, body: [payload])
276
+ request = rklass.new("POST", uri, options, body: [payload])
229
277
  request.headers["content-type"] = "application/dns-message"
230
278
  end
231
279
  request.headers["accept"] = "application/dns-message"
@@ -243,6 +291,7 @@ module HTTPX
243
291
  end
244
292
 
245
293
  def reset_hostname(hostname, reset_candidates: true)
294
+ @timeouts.delete(hostname)
246
295
  connection = @queries.delete(hostname)
247
296
 
248
297
  return connection unless connection && reset_candidates
@@ -250,6 +299,8 @@ module HTTPX
250
299
  # eliminate other candidates
251
300
  candidates = @queries.select { |_, conn| connection == conn }.keys
252
301
  @queries.delete_if { |h, _| candidates.include?(h) }
302
+ # reset timeouts
303
+ @timeouts.delete_if { |h, _| candidates.include?(h) }
253
304
 
254
305
  connection
255
306
  end
@@ -259,6 +310,9 @@ module HTTPX
259
310
  @connections.shift until @connections.empty? || @connections.first.state != :closed
260
311
 
261
312
  if (@connections - @queries.values).empty?
313
+ # the same resolver connection may be serving different https resolvers (AAAA and A).
314
+ return if inflight?
315
+
262
316
  if should_deactivate
263
317
  deactivate
264
318
  else
@@ -19,8 +19,10 @@ module HTTPX
19
19
  resolver.multi = self
20
20
  resolver
21
21
  end
22
+ end
22
23
 
23
- @errors = Hash.new { |hs, k| hs[k] = [] }
24
+ def state
25
+ @resolvers.map(&:state).uniq.join(",")
24
26
  end
25
27
 
26
28
  def current_selector=(s)
@@ -43,7 +45,7 @@ module HTTPX
43
45
 
44
46
  def early_resolve(connection)
45
47
  hostname = connection.peer.host
46
- addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
48
+ addresses = @resolver_options[:cache] && (connection.addresses || nolookup_resolve(hostname, connection.options))
47
49
  return false unless addresses
48
50
 
49
51
  ip_families = connection.options.ip_families
@@ -79,5 +81,11 @@ module HTTPX
79
81
  @current_session.select_resolver(resolver, @current_selector)
80
82
  end
81
83
  end
84
+
85
+ private
86
+
87
+ def nolookup_resolve(hostname, options)
88
+ options.resolver_cache.resolve(hostname)
89
+ end
82
90
  end
83
91
  end
@@ -9,6 +9,7 @@ module HTTPX
9
9
  #
10
10
  class Resolver::Native < Resolver::Resolver
11
11
  extend Forwardable
12
+
12
13
  using URIExtensions
13
14
 
14
15
  DEFAULTS = {
@@ -135,7 +136,11 @@ module HTTPX
135
136
  private
136
137
 
137
138
  def calculate_interests
138
- return if @queries.empty?
139
+ if @queries.empty?
140
+ return @io.interests if (@socket_type == :tcp) && (@state == :idle)
141
+
142
+ return
143
+ end
139
144
 
140
145
  return :r if @write_buffer.empty?
141
146
 
@@ -298,16 +303,14 @@ module HTTPX
298
303
  end
299
304
 
300
305
  def parse(buffer)
301
- @timer.cancel
302
-
303
- @timer = @name = nil
304
-
305
306
  code, result = Resolver.decode_dns_answer(buffer)
306
307
 
307
308
  case code
308
309
  when :ok
310
+ reset_query
309
311
  parse_addresses(result)
310
312
  when :no_domain_found
313
+ reset_query
311
314
  # Indicates no such domain was found.
312
315
  hostname, connection = @queries.first
313
316
  reset_hostname(hostname, reset_candidates: false)
@@ -324,6 +327,7 @@ module HTTPX
324
327
  close_or_resolve
325
328
  end
326
329
  when :message_truncated
330
+ reset_query
327
331
  # TODO: what to do if it's already tcp??
328
332
  return if @socket_type == :tcp
329
333
 
@@ -332,13 +336,29 @@ module HTTPX
332
336
  hostname, _ = @queries.first
333
337
  reset_hostname(hostname)
334
338
  transition(:closed)
339
+ when :retriable_error
340
+ if @name && @timer
341
+ log { "resolver #{FAMILY_TYPES[@record_type]}: failed, but will retry..." }
342
+ return
343
+ end
344
+ # retry now!
345
+ # connection = @queries[@name].shift
346
+ # @timer.fire
347
+ reset_query
348
+ hostname, connection = @queries.first
349
+ reset_hostname(hostname)
350
+ @connections.delete(connection)
351
+ ex = NativeResolveError.new(connection, connection.peer.host, "unknown DNS error (error code #{result})")
352
+ raise ex
335
353
  when :dns_error
354
+ reset_query
336
355
  hostname, connection = @queries.first
337
356
  reset_hostname(hostname)
338
357
  @connections.delete(connection)
339
358
  ex = NativeResolveError.new(connection, connection.peer.host, "unknown DNS error (error code #{result})")
340
359
  raise ex
341
360
  when :decode_error
361
+ reset_query
342
362
  hostname, connection = @queries.first
343
363
  reset_hostname(hostname)
344
364
  @connections.delete(connection)
@@ -401,7 +421,7 @@ module HTTPX
401
421
  reset_hostname(name, connection: connection)
402
422
  @timeouts.delete(connection.peer.host)
403
423
  @connections.delete(connection)
404
- Resolver.cached_lookup_set(connection.peer.host, @family, addresses) if @resolver_options[:cache]
424
+ @options.resolver_cache.set(connection.peer.host, @family, addresses) if @resolver_options[:cache]
405
425
  catch(:coalesced) do
406
426
  emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) })
407
427
  end
@@ -529,6 +549,12 @@ module HTTPX
529
549
  on_error(e)
530
550
  end
531
551
 
552
+ def reset_query
553
+ @timer.cancel
554
+
555
+ @timer = @name = nil
556
+ end
557
+
532
558
  def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
533
559
  @timeouts.delete(hostname)
534
560
 
@@ -75,7 +75,7 @@ module HTTPX
75
75
 
76
76
  # double emission check, but allow early resolution to work
77
77
  conn_addrs = connection.addresses
78
- return if !early_resolve && conn_addrs && (!conn_addrs.empty? && !addresses.intersect?(conn_addrs))
78
+ return if !early_resolve && conn_addrs && !conn_addrs.empty? && !addresses.intersect?(conn_addrs)
79
79
 
80
80
  log do
81
81
  "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
@@ -124,8 +124,8 @@ module HTTPX
124
124
  disconnect
125
125
  end
126
126
 
127
- def early_resolve(connection, hostname: connection.peer.host)
128
- addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
127
+ def early_resolve(connection, hostname: connection.peer.host) # rubocop:disable Naming/PredicateMethod
128
+ addresses = @resolver_options[:cache] && (connection.addresses || @options.resolver_cache.resolve(hostname))
129
129
 
130
130
  return false unless addresses
131
131
 
@@ -5,132 +5,35 @@ require "resolv"
5
5
 
6
6
  module HTTPX
7
7
  module Resolver
8
- RESOLVE_TIMEOUT = [2, 3].freeze
8
+ extend self
9
9
 
10
+ RESOLVE_TIMEOUT = [2, 3].freeze
10
11
  require "httpx/resolver/entry"
12
+ require "httpx/resolver/cache"
11
13
  require "httpx/resolver/resolver"
12
14
  require "httpx/resolver/system"
13
15
  require "httpx/resolver/native"
14
16
  require "httpx/resolver/https"
15
17
  require "httpx/resolver/multi"
16
18
 
17
- @lookup_mutex = Thread::Mutex.new
18
- @lookups = Hash.new { |h, k| h[k] = [] }
19
-
20
19
  @identifier_mutex = Thread::Mutex.new
21
20
  @identifier = 1
22
- @hosts_resolver = Resolv::Hosts.new
23
-
24
- module_function
25
21
 
26
22
  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)
41
- case resolver_type
42
- when Symbol
43
- meth = :"resolver_#{resolver_type}_class"
44
-
45
- return options.__send__(meth) if options.respond_to?(meth)
46
- when Class
47
- return resolver_type if resolver_type < Resolver
48
- end
49
-
50
- raise Error, "unsupported resolver type (#{resolver_type})"
51
- end
52
-
53
- def nolookup_resolve(hostname)
54
- ip_resolve(hostname) || cached_lookup(hostname) || hosts_resolve(hostname)
55
- end
56
-
57
- # tries to convert +hostname+ into an IPAddr, returns <tt>nil</tt> otherwise.
58
- def ip_resolve(hostname)
59
- [Entry.new(hostname)]
60
- rescue ArgumentError
61
- end
62
-
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)
67
- return if ips.empty?
68
-
69
- ips.map { |ip| Entry.new(ip) }
70
- rescue IOError
71
- end
72
-
73
- def cached_lookup(hostname)
74
- now = Utils.now
75
- lookup_synchronize do |lookups|
76
- lookup(hostname, lookups, now)
77
- end
78
- end
79
-
80
- def cached_lookup_set(hostname, family, entries)
81
- lookup_synchronize do |lookups|
82
- case family
83
- when Socket::AF_INET6
84
- lookups[hostname].concat(entries)
85
- when Socket::AF_INET
86
- lookups[hostname].unshift(*entries)
87
- end
88
- entries.each do |entry|
89
- next unless entry["name"] != hostname
90
-
91
- case family
92
- when Socket::AF_INET6
93
- lookups[entry["name"]] << entry
94
- when Socket::AF_INET
95
- lookups[entry["name"]].unshift(entry)
96
- end
97
- end
23
+ if Utils.in_ractor?
24
+ Ractor.store_if_absent(:httpx_supported_ip_families) { find_supported_ip_families }
25
+ else
26
+ @supported_ip_families ||= find_supported_ip_families
98
27
  end
99
28
  end
100
29
 
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
-
113
- # do not use directly!
114
- def lookup(hostname, lookups, ttl)
115
- return unless lookups.key?(hostname)
116
-
117
- entries = lookups[hostname] = lookups[hostname].select do |address|
118
- address["TTL"] > ttl
119
- end
120
-
121
- ips = entries.flat_map do |address|
122
- if (als = address["alias"])
123
- lookup(als, lookups, ttl)
124
- else
125
- Entry.new(address["data"], address["TTL"])
126
- end
127
- end.compact
128
-
129
- ips unless ips.empty?
130
- end
131
-
132
30
  def generate_id
133
- id_synchronize { @identifier = (@identifier + 1) & 0xFFFF }
31
+ if Utils.in_ractor?
32
+ identifier = Ractor.store_if_absent(:httpx_resolver_identifier) { -1 }
33
+ Ractor.current[:httpx_resolver_identifier] = (identifier + 1) & 0xFFFF
34
+ else
35
+ id_synchronize { @identifier = (@identifier + 1) & 0xFFFF }
36
+ end
134
37
  end
135
38
 
136
39
  def encode_dns_query(hostname, type: Resolv::DNS::Resource::IN::A, message_id: generate_id)
@@ -152,7 +55,14 @@ module HTTPX
152
55
 
153
56
  return :message_truncated if message.tc == 1
154
57
 
155
- return :dns_error, message.rcode if message.rcode != Resolv::DNS::RCode::NoError
58
+ if message.rcode != Resolv::DNS::RCode::NoError
59
+ case message.rcode
60
+ when Resolv::DNS::RCode::ServFail
61
+ return :retriable_error, message.rcode
62
+ else
63
+ return :dns_error, message.rcode
64
+ end
65
+ end
156
66
 
157
67
  addresses = []
158
68
 
@@ -178,12 +88,24 @@ module HTTPX
178
88
  [:ok, addresses]
179
89
  end
180
90
 
181
- def lookup_synchronize
182
- @lookup_mutex.synchronize { yield(@lookups) }
183
- end
91
+ private
184
92
 
185
93
  def id_synchronize(&block)
186
94
  @identifier_mutex.synchronize(&block)
187
95
  end
96
+
97
+ def find_supported_ip_families
98
+ list = Socket.ip_address_list
99
+
100
+ begin
101
+ if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
102
+ [Socket::AF_INET6, Socket::AF_INET]
103
+ else
104
+ [Socket::AF_INET]
105
+ end
106
+ rescue NotImplementedError
107
+ [Socket::AF_INET]
108
+ end.freeze
109
+ end
188
110
  end
189
111
  end
@@ -59,13 +59,11 @@ module HTTPX
59
59
 
60
60
  chunk = decode_chunk(chunk)
61
61
 
62
- size = chunk.bytesize
63
- @length += size
64
62
  transition(:open)
65
63
  @buffer.write(chunk)
66
64
 
67
65
  @response.emit(:chunk_received, chunk)
68
- size
66
+ chunk.bytesize
69
67
  end
70
68
 
71
69
  # reads a chunk from the payload (implementation of the IO reader protocol).
@@ -138,6 +136,8 @@ module HTTPX
138
136
  else
139
137
  IO.copy_stream(@buffer, dest)
140
138
  end
139
+ ensure
140
+ close
141
141
  end
142
142
 
143
143
  # closes/cleans the buffer, resets everything
@@ -207,6 +207,8 @@ module HTTPX
207
207
  chunk = inflater.call(chunk)
208
208
  end if @inflaters
209
209
 
210
+ @length += chunk.bytesize
211
+
210
212
  chunk
211
213
  end
212
214
 
@@ -211,16 +211,19 @@ module HTTPX
211
211
 
212
212
  def initialize(header_value)
213
213
  @header_value = header_value
214
+ @mime_type = @charset = nil
215
+ @initialized = false
214
216
  end
215
217
 
216
218
  # returns the mime type declared in the header.
217
219
  #
218
220
  # ContentType.new("application/json; charset=utf-8").mime_type #=> "application/json"
219
221
  def mime_type
220
- return @mime_type if defined?(@mime_type)
222
+ return @mime_type if @initialized
221
223
 
222
- m = @header_value.to_s[MIME_TYPE_RE, 1]
223
- m && @mime_type = m.strip.downcase
224
+ load
225
+
226
+ @mime_type
224
227
  end
225
228
 
226
229
  # returns the charset declared in the header.
@@ -228,10 +231,23 @@ module HTTPX
228
231
  # ContentType.new("application/json; charset=utf-8").charset #=> "utf-8"
229
232
  # ContentType.new("text/plain").charset #=> nil
230
233
  def charset
231
- return @charset if defined?(@charset)
234
+ return @charset if @initialized
235
+
236
+ load
237
+
238
+ @charset
239
+ end
240
+
241
+ private
242
+
243
+ def load
244
+ m = @header_value.to_s[MIME_TYPE_RE, 1]
245
+ m && @mime_type = m.strip.downcase
246
+
247
+ c = @header_value.to_s[CHARSET_RE, 1]
248
+ c && @charset = c.strip.delete('"')
232
249
 
233
- m = @header_value.to_s[CHARSET_RE, 1]
234
- m && @charset = m.strip.delete('"')
250
+ @initialized = true
235
251
  end
236
252
  end
237
253
 
@@ -127,13 +127,15 @@ module HTTPX
127
127
  private
128
128
 
129
129
  def select(interval, &block)
130
- has_no_selectables = @selectables.empty?
131
130
  # do not cause an infinite loop here.
132
131
  #
133
132
  # this may happen if timeout calculation actually triggered an error which causes
134
133
  # the connections to be reaped (such as the total timeout error) before #select
135
134
  # gets called.
136
- return if interval.nil? && has_no_selectables
135
+ if @selectables.empty?
136
+ sleep(interval) if interval
137
+ return
138
+ end
137
139
 
138
140
  # @type var r: (selectable | Array[selectable])?
139
141
  # @type var w: (selectable | Array[selectable])?
@@ -171,7 +173,7 @@ module HTTPX
171
173
  when Array
172
174
  select_many(r, w, interval, &block)
173
175
  when nil
174
- return unless interval && has_no_selectables
176
+ return unless interval && @selectables.any?
175
177
 
176
178
  # no selectables
177
179
  # TODO: replace with sleep?
@@ -199,6 +201,12 @@ module HTTPX
199
201
  def select_many(r, w, interval, &block)
200
202
  begin
201
203
  readers, writers = ::IO.select(r, w, nil, interval)
204
+ rescue IOError => e
205
+ (Array(r) + Array(w)).each do |sel|
206
+ # TODO: is there a way to cheaply find the IO associated with the error?
207
+ sel.on_error(e)
208
+ sel.force_close(true)
209
+ end
202
210
  rescue StandardError => e
203
211
  (Array(r) + Array(w)).each do |sel|
204
212
  sel.on_error(e)
@@ -240,6 +248,9 @@ module HTTPX
240
248
  when :w then io.to_io.wait_writable(interval)
241
249
  when :rw then rw_wait(io, interval)
242
250
  end
251
+ rescue IOError => e
252
+ io.on_error(e)
253
+ io.force_close(true)
243
254
  rescue StandardError => e
244
255
  io.on_error(e)
245
256