httpx 1.2.6 → 1.4.4

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/doc/release_notes/1_3_0.md +18 -0
  4. data/doc/release_notes/1_3_1.md +17 -0
  5. data/doc/release_notes/1_3_2.md +6 -0
  6. data/doc/release_notes/1_3_3.md +5 -0
  7. data/doc/release_notes/1_3_4.md +6 -0
  8. data/doc/release_notes/1_4_0.md +43 -0
  9. data/doc/release_notes/1_4_1.md +19 -0
  10. data/doc/release_notes/1_4_2.md +20 -0
  11. data/doc/release_notes/1_4_3.md +11 -0
  12. data/doc/release_notes/1_4_4.md +14 -0
  13. data/lib/httpx/adapters/datadog.rb +56 -80
  14. data/lib/httpx/adapters/faraday.rb +5 -2
  15. data/lib/httpx/adapters/webmock.rb +24 -8
  16. data/lib/httpx/callbacks.rb +2 -7
  17. data/lib/httpx/chainable.rb +3 -1
  18. data/lib/httpx/connection/http1.rb +11 -7
  19. data/lib/httpx/connection/http2.rb +57 -34
  20. data/lib/httpx/connection.rb +270 -71
  21. data/lib/httpx/errors.rb +15 -4
  22. data/lib/httpx/io/ssl.rb +6 -3
  23. data/lib/httpx/io/tcp.rb +1 -1
  24. data/lib/httpx/io/unix.rb +1 -1
  25. data/lib/httpx/loggable.rb +17 -10
  26. data/lib/httpx/options.rb +30 -23
  27. data/lib/httpx/plugins/aws_sdk_authentication.rb +3 -0
  28. data/lib/httpx/plugins/aws_sigv4.rb +36 -17
  29. data/lib/httpx/plugins/callbacks.rb +13 -2
  30. data/lib/httpx/plugins/circuit_breaker.rb +11 -5
  31. data/lib/httpx/plugins/content_digest.rb +202 -0
  32. data/lib/httpx/plugins/cookies.rb +9 -6
  33. data/lib/httpx/plugins/digest_auth.rb +3 -0
  34. data/lib/httpx/plugins/expect.rb +10 -4
  35. data/lib/httpx/plugins/follow_redirects.rb +68 -33
  36. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  37. data/lib/httpx/plugins/grpc.rb +2 -2
  38. data/lib/httpx/plugins/h2c.rb +23 -20
  39. data/lib/httpx/plugins/internal_telemetry.rb +48 -1
  40. data/lib/httpx/plugins/oauth.rb +1 -1
  41. data/lib/httpx/plugins/persistent.rb +16 -0
  42. data/lib/httpx/plugins/proxy/http.rb +19 -16
  43. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  44. data/lib/httpx/plugins/proxy/socks5.rb +1 -1
  45. data/lib/httpx/plugins/proxy.rb +96 -85
  46. data/lib/httpx/plugins/retries.rb +28 -10
  47. data/lib/httpx/plugins/ssrf_filter.rb +4 -1
  48. data/lib/httpx/plugins/stream.rb +42 -18
  49. data/lib/httpx/plugins/upgrade.rb +5 -10
  50. data/lib/httpx/plugins/webdav.rb +6 -0
  51. data/lib/httpx/plugins/xml.rb +76 -0
  52. data/lib/httpx/pool.rb +73 -244
  53. data/lib/httpx/request/body.rb +50 -55
  54. data/lib/httpx/request.rb +77 -14
  55. data/lib/httpx/resolver/https.rb +17 -20
  56. data/lib/httpx/resolver/multi.rb +34 -16
  57. data/lib/httpx/resolver/native.rb +140 -61
  58. data/lib/httpx/resolver/resolver.rb +64 -19
  59. data/lib/httpx/resolver/system.rb +32 -16
  60. data/lib/httpx/resolver.rb +21 -14
  61. data/lib/httpx/response/body.rb +12 -1
  62. data/lib/httpx/response.rb +16 -9
  63. data/lib/httpx/selector.rb +170 -91
  64. data/lib/httpx/session.rb +282 -139
  65. data/lib/httpx/timers.rb +17 -2
  66. data/lib/httpx/transcoder/body.rb +15 -29
  67. data/lib/httpx/transcoder/form.rb +2 -0
  68. data/lib/httpx/transcoder/gzip.rb +0 -3
  69. data/lib/httpx/transcoder/json.rb +16 -2
  70. data/lib/httpx/transcoder/multipart/encoder.rb +11 -2
  71. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  72. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  73. data/lib/httpx/transcoder.rb +0 -1
  74. data/lib/httpx/version.rb +1 -1
  75. data/lib/httpx.rb +20 -21
  76. data/sig/callbacks.rbs +2 -3
  77. data/sig/chainable.rbs +6 -2
  78. data/sig/connection/http1.rbs +2 -2
  79. data/sig/connection/http2.rbs +22 -18
  80. data/sig/connection.rbs +40 -9
  81. data/sig/errors.rbs +9 -3
  82. data/sig/httpx.rbs +3 -3
  83. data/sig/io/tcp.rbs +1 -1
  84. data/sig/io/unix.rbs +1 -1
  85. data/sig/loggable.rbs +4 -2
  86. data/sig/options.rbs +8 -13
  87. data/sig/plugins/aws_sigv4.rbs +8 -2
  88. data/sig/plugins/content_digest.rbs +51 -0
  89. data/sig/plugins/cookies/cookie.rbs +9 -0
  90. data/sig/plugins/follow_redirects.rbs +1 -1
  91. data/sig/plugins/grpc/call.rbs +4 -0
  92. data/sig/plugins/persistent.rbs +4 -1
  93. data/sig/plugins/proxy/http.rbs +3 -0
  94. data/sig/plugins/proxy/socks5.rbs +11 -3
  95. data/sig/plugins/proxy.rbs +18 -9
  96. data/sig/plugins/push_promise.rbs +6 -3
  97. data/sig/plugins/rate_limiter.rbs +2 -0
  98. data/sig/plugins/retries.rbs +1 -1
  99. data/sig/plugins/ssrf_filter.rbs +26 -0
  100. data/sig/plugins/stream.rbs +3 -0
  101. data/sig/plugins/webdav.rbs +23 -0
  102. data/sig/plugins/xml.rbs +37 -0
  103. data/sig/pool.rbs +27 -33
  104. data/sig/request/body.rbs +4 -10
  105. data/sig/request.rbs +14 -1
  106. data/sig/resolver/multi.rbs +26 -1
  107. data/sig/resolver/native.rbs +6 -3
  108. data/sig/resolver/resolver.rbs +22 -3
  109. data/sig/resolver.rbs +5 -1
  110. data/sig/response/body.rbs +2 -2
  111. data/sig/response/buffer.rbs +2 -2
  112. data/sig/response.rbs +9 -4
  113. data/sig/selector.rbs +31 -4
  114. data/sig/session.rbs +54 -20
  115. data/sig/timers.rbs +15 -4
  116. data/sig/transcoder/body.rbs +2 -4
  117. data/sig/transcoder/chunker.rbs +1 -1
  118. data/sig/transcoder/deflate.rbs +1 -0
  119. data/sig/transcoder/form.rbs +8 -0
  120. data/sig/transcoder/gzip.rbs +4 -1
  121. data/sig/transcoder/json.rbs +1 -1
  122. data/sig/transcoder/multipart.rbs +6 -4
  123. data/sig/transcoder/utils/body_reader.rbs +3 -3
  124. data/sig/transcoder/utils/deflater.rbs +2 -3
  125. metadata +32 -14
  126. data/lib/httpx/session2.rb +0 -23
  127. data/lib/httpx/transcoder/utils/inflater.rb +0 -19
  128. data/lib/httpx/transcoder/xml.rb +0 -52
  129. data/sig/transcoder/utils/inflater.rbs +0 -12
  130. data/sig/transcoder/xml.rbs +0 -22
@@ -35,6 +35,7 @@ module HTTPX
35
35
  @_timeouts = Array(@resolver_options[:timeouts])
36
36
  @timeouts = Hash.new { |timeouts, host| timeouts[host] = @_timeouts.dup }
37
37
  @connections = []
38
+ @name = nil
38
39
  @queries = {}
39
40
  @read_buffer = "".b
40
41
  @write_buffer = Buffer.new(@resolver_options[:packet_size])
@@ -58,19 +59,6 @@ module HTTPX
58
59
  when :open
59
60
  consume
60
61
  end
61
- nil
62
- rescue Errno::EHOSTUNREACH => e
63
- @ns_index += 1
64
- nameserver = @nameserver
65
- if nameserver && @ns_index < nameserver.size
66
- log { "resolver: failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})" }
67
- transition(:idle)
68
- @timeouts.clear
69
- else
70
- handle_error(e)
71
- end
72
- rescue NativeResolveError => e
73
- handle_error(e)
74
62
  end
75
63
 
76
64
  def interests
@@ -105,9 +93,7 @@ module HTTPX
105
93
  @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
106
94
  end
107
95
 
108
- def handle_socket_timeout(interval)
109
- do_retry(interval)
110
- end
96
+ def handle_socket_timeout(interval); end
111
97
 
112
98
  private
113
99
 
@@ -120,51 +106,94 @@ module HTTPX
120
106
  end
121
107
 
122
108
  def consume
123
- dread if calculate_interests == :r
124
- do_retry
125
- dwrite if calculate_interests == :w
109
+ loop do
110
+ dread if calculate_interests == :r
111
+
112
+ break unless calculate_interests == :w
113
+
114
+ # do_retry
115
+ dwrite
116
+
117
+ break unless calculate_interests == :r
118
+ end
119
+ rescue Errno::EHOSTUNREACH => e
120
+ @ns_index += 1
121
+ nameserver = @nameserver
122
+ if nameserver && @ns_index < nameserver.size
123
+ log do
124
+ "resolver #{FAMILY_TYPES[@record_type]}: " \
125
+ "failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})"
126
+ end
127
+ transition(:idle)
128
+ @timeouts.clear
129
+ retry
130
+ else
131
+ handle_error(e)
132
+ emit(:close, self)
133
+ end
134
+ rescue NativeResolveError => e
135
+ handle_error(e)
136
+ close_or_resolve
137
+ retry unless closed?
126
138
  end
127
139
 
128
- def do_retry(loop_time = nil)
129
- return if @queries.empty? || !@start_timeout
140
+ def schedule_retry
141
+ h = @name
130
142
 
131
- loop_time ||= Utils.elapsed_time(@start_timeout)
143
+ return unless h
132
144
 
133
- query = @queries.first
145
+ connection = @queries[h]
134
146
 
135
- return unless query
147
+ timeouts = @timeouts[h]
148
+ timeout = timeouts.shift
136
149
 
137
- h, connection = query
138
- host = connection.origin.host
139
- timeout = (@timeouts[host][0] -= loop_time)
150
+ @timer = @current_selector.after(timeout) do
151
+ next unless @connections.include?(connection)
140
152
 
141
- return unless timeout <= 0
153
+ do_retry(h, connection, timeout)
154
+ end
155
+ end
142
156
 
143
- @timeouts[host].shift
157
+ def do_retry(h, connection, interval)
158
+ timeouts = @timeouts[h]
144
159
 
145
- if !@timeouts[host].empty?
146
- log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
160
+ if !timeouts.empty?
161
+ log do
162
+ "resolver #{FAMILY_TYPES[@record_type]}: timeout after #{interval}s, retry (with #{timeouts.first}s) #{h}..."
163
+ end
147
164
  # must downgrade to tcp AND retry on same host as last
148
165
  downgrade_socket
149
166
  resolve(connection, h)
150
167
  elsif @ns_index + 1 < @nameserver.size
151
168
  # try on the next nameserver
152
169
  @ns_index += 1
153
- log { "resolver: failed resolving #{host} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)" }
170
+ log do
171
+ "resolver #{FAMILY_TYPES[@record_type]}: failed resolving #{h} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)"
172
+ end
154
173
  transition(:idle)
155
174
  @timeouts.clear
156
175
  resolve(connection, h)
157
176
  else
158
177
 
159
- @timeouts.delete(host)
178
+ @timeouts.delete(h)
160
179
  reset_hostname(h, reset_candidates: false)
161
180
 
162
- return unless @queries.empty?
181
+ unless @queries.empty?
182
+ resolve(connection)
183
+ return
184
+ end
163
185
 
164
186
  @connections.delete(connection)
187
+
188
+ host = connection.peer.host
189
+
165
190
  # This loop_time passed to the exception is bogus. Ideally we would pass the total
166
191
  # resolve timeout, including from the previous retries.
167
- raise ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.origin.host}")
192
+ ex = ResolveTimeoutError.new(interval, "Timed out while resolving #{host}")
193
+ ex.set_backtrace(ex ? ex.backtrace : caller)
194
+ emit_resolve_error(connection, host, ex)
195
+
196
+ close_or_resolve
168
197
  end
169
198
  end
170
199
 
@@ -213,7 +242,7 @@ module HTTPX
213
242
  parse(@read_buffer)
214
243
  end
215
244
 
216
- return if @state == :closed
245
+ return if @state == :closed || !@write_buffer.empty?
217
246
  end
218
247
  end
219
248
 
@@ -231,11 +260,15 @@ module HTTPX
231
260
 
232
261
  return unless siz.positive?
233
262
 
263
+ schedule_retry if @write_buffer.empty?
264
+
234
265
  return if @state == :closed
235
266
  end
236
267
  end
237
268
 
238
269
  def parse(buffer)
270
+ @timer.cancel
271
+
239
272
  code, result = Resolver.decode_dns_answer(buffer)
240
273
 
241
274
  case code
@@ -246,12 +279,17 @@ module HTTPX
246
279
  hostname, connection = @queries.first
247
280
  reset_hostname(hostname, reset_candidates: false)
248
281
 
249
- unless @queries.value?(connection)
282
+ other_candidate, _ = @queries.find { |_, conn| conn == connection }
283
+
284
+ if other_candidate
285
+ resolve(connection, other_candidate)
286
+ else
250
287
  @connections.delete(connection)
251
- raise NativeResolveError.new(connection, connection.origin.host, "name or service not known")
288
+ ex = NativeResolveError.new(connection, connection.peer.host, "name or service not known")
289
+ ex.set_backtrace(ex ? ex.backtrace : caller)
290
+ emit_resolve_error(connection, connection.peer.host, ex)
291
+ close_or_resolve
252
292
  end
253
-
254
- resolve
255
293
  when :message_truncated
256
294
  # TODO: what to do if it's already tcp??
257
295
  return if @socket_type == :tcp
@@ -265,13 +303,13 @@ module HTTPX
265
303
  hostname, connection = @queries.first
266
304
  reset_hostname(hostname)
267
305
  @connections.delete(connection)
268
- ex = NativeResolveError.new(connection, connection.origin.host, "unknown DNS error (error code #{result})")
306
+ ex = NativeResolveError.new(connection, connection.peer.host, "unknown DNS error (error code #{result})")
269
307
  raise ex
270
308
  when :decode_error
271
309
  hostname, connection = @queries.first
272
310
  reset_hostname(hostname)
273
311
  @connections.delete(connection)
274
- ex = NativeResolveError.new(connection, connection.origin.host, result.message)
312
+ ex = NativeResolveError.new(connection, connection.peer.host, result.message)
275
313
  ex.set_backtrace(result.backtrace)
276
314
  raise ex
277
315
  end
@@ -283,7 +321,7 @@ module HTTPX
283
321
  hostname, connection = @queries.first
284
322
  reset_hostname(hostname)
285
323
  @connections.delete(connection)
286
- raise NativeResolveError.new(connection, connection.origin.host)
324
+ raise NativeResolveError.new(connection, connection.peer.host)
287
325
  else
288
326
  address = addresses.first
289
327
  name = address["name"]
@@ -306,12 +344,14 @@ module HTTPX
306
344
  connection = @queries.delete(name)
307
345
  end
308
346
 
309
- if address.key?("alias") # CNAME
310
- hostname_alias = address["alias"]
347
+ alias_addresses, addresses = addresses.partition { |addr| addr.key?("alias") }
348
+
349
+ if addresses.empty? && !alias_addresses.empty? # CNAME
350
+ hostname_alias = alias_addresses.first["alias"]
311
351
  # clean up intermediate queries
312
- @timeouts.delete(name) unless connection.origin.host == name
352
+ @timeouts.delete(name) unless connection.peer.host == name
313
353
 
314
- if catch(:coalesced) { early_resolve(connection, hostname: hostname_alias) }
354
+ if early_resolve(connection, hostname: hostname_alias)
315
355
  @connections.delete(connection)
316
356
  else
317
357
  if @socket_type == :tcp
@@ -320,24 +360,26 @@ module HTTPX
320
360
  transition(:idle)
321
361
  transition(:open)
322
362
  end
323
- log { "resolver: ALIAS #{hostname_alias} for #{name}" }
363
+ log { "resolver #{FAMILY_TYPES[@record_type]}: ALIAS #{hostname_alias} for #{name}" }
324
364
  resolve(connection, hostname_alias)
325
365
  return
326
366
  end
327
367
  else
328
368
  reset_hostname(name, connection: connection)
329
- @timeouts.delete(connection.origin.host)
369
+ @timeouts.delete(connection.peer.host)
330
370
  @connections.delete(connection)
331
- Resolver.cached_lookup_set(connection.origin.host, @family, addresses) if @resolver_options[:cache]
332
- emit_addresses(connection, @family, addresses.map { |addr| addr["data"] })
371
+ Resolver.cached_lookup_set(connection.peer.host, @family, addresses) if @resolver_options[:cache]
372
+ catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |addr| addr["data"] }) }
333
373
  end
334
374
  end
335
- return emit(:close) if @connections.empty?
336
-
337
- resolve
375
+ close_or_resolve
338
376
  end
339
377
 
340
- def resolve(connection = @connections.first, hostname = nil)
378
+ def resolve(connection = nil, hostname = nil)
379
+ @connections.shift until @connections.empty? || @connections.first.state != :closed
380
+
381
+ connection ||= @connections.find { |c| !@queries.value?(c) }
382
+
341
383
  raise Error, "no URI to resolve" unless connection
342
384
 
343
385
  return unless @write_buffer.empty?
@@ -345,8 +387,11 @@ module HTTPX
345
387
  hostname ||= @queries.key(connection)
346
388
 
347
389
  if hostname.nil?
348
- hostname = connection.origin.host
349
- log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
390
+ hostname = connection.peer.host
391
+ log do
392
+ "resolver #{FAMILY_TYPES[@record_type]}: " \
393
+ "resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}"
394
+ end if connection.peer.non_ascii_hostname
350
395
 
351
396
  hostname = generate_candidates(hostname).each do |name|
352
397
  @queries[name] = connection
@@ -354,11 +399,17 @@ module HTTPX
354
399
  else
355
400
  @queries[hostname] = connection
356
401
  end
357
- log { "resolver: query #{@record_type.name.split("::").last} for #{hostname}" }
402
+
403
+ @name = hostname
404
+
405
+ log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
358
406
  begin
359
407
  @write_buffer << encode_dns_query(hostname)
360
408
  rescue Resolv::DNS::EncodeError => e
409
+ reset_hostname(hostname, connection: connection)
410
+ @connections.delete(connection)
361
411
  emit_resolve_error(connection, hostname, e)
412
+ close_or_resolve
362
413
  end
363
414
  end
364
415
 
@@ -388,10 +439,10 @@ module HTTPX
388
439
 
389
440
  case @socket_type
390
441
  when :udp
391
- log { "resolver: server: udp://#{ip}:#{port}..." }
442
+ log { "resolver #{FAMILY_TYPES[@record_type]}: server: udp://#{ip}:#{port}..." }
392
443
  UDP.new(ip, port, @options)
393
444
  when :tcp
394
- log { "resolver: server: tcp://#{ip}:#{port}..." }
445
+ log { "resolver #{FAMILY_TYPES[@record_type]}: server: tcp://#{ip}:#{port}..." }
395
446
  origin = URI("tcp://#{ip}:#{port}")
396
447
  TCP.new(origin, [ip], @options)
397
448
  end
@@ -430,14 +481,32 @@ module HTTPX
430
481
  @read_buffer.clear
431
482
  end
432
483
  @state = nextstate
484
+ rescue Errno::ECONNREFUSED,
485
+ Errno::EADDRNOTAVAIL,
486
+ Errno::EHOSTUNREACH,
487
+ SocketError,
488
+ IOError,
489
+ ConnectTimeoutError => e
490
+ # these errors may happen during TCP handshake
491
+ # treat them as resolve errors.
492
+ handle_error(e)
493
+ emit(:close, self)
433
494
  end
434
495
 
435
496
  def handle_error(error)
436
497
  if error.respond_to?(:connection) &&
437
498
  error.respond_to?(:host)
499
+ reset_hostname(error.host, connection: error.connection)
500
+ @connections.delete(error.connection)
438
501
  emit_resolve_error(error.connection, error.host, error)
439
502
  else
440
503
  @queries.each do |host, connection|
504
+ reset_hostname(host, connection: connection)
505
+ @connections.delete(connection)
506
+ emit_resolve_error(connection, host, error)
507
+ end
508
+
509
+ while (connection = @connections.shift)
441
510
  emit_resolve_error(connection, host, error)
442
511
  end
443
512
  end
@@ -445,7 +514,6 @@ module HTTPX
445
514
 
446
515
  def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
447
516
  @timeouts.delete(hostname)
448
- @timeouts.delete(hostname)
449
517
 
450
518
  return unless connection && reset_candidates
451
519
 
@@ -455,5 +523,16 @@ module HTTPX
455
523
  # reset timeouts
456
524
  @timeouts.delete_if { |h, _| candidates.include?(h) }
457
525
  end
526
+
527
+ def close_or_resolve
528
+ # drop already closed connections
529
+ @connections.shift until @connections.empty? || @connections.first.state != :closed
530
+
531
+ if (@connections - @queries.values).empty?
532
+ emit(:close, self)
533
+ else
534
+ resolve
535
+ end
536
+ end
458
537
  end
459
538
  end
@@ -26,14 +26,26 @@ module HTTPX
26
26
  end
27
27
  end
28
28
 
29
- attr_reader :family
29
+ attr_reader :family, :options
30
30
 
31
- attr_writer :pool
31
+ attr_writer :current_selector, :current_session
32
+
33
+ attr_accessor :multi
32
34
 
33
35
  def initialize(family, options)
34
36
  @family = family
35
37
  @record_type = RECORD_TYPES[family]
36
- @options = Options.new(options)
38
+ @options = options
39
+
40
+ set_resolver_callbacks
41
+ end
42
+
43
+ def each_connection(&block)
44
+ enum_for(__method__) unless block
45
+
46
+ return unless @connections
47
+
48
+ @connections.each(&block)
37
49
  end
38
50
 
39
51
  def close; end
@@ -48,6 +60,10 @@ module HTTPX
48
60
  true
49
61
  end
50
62
 
63
+ def inflight?
64
+ false
65
+ end
66
+
51
67
  def emit_addresses(connection, family, addresses, early_resolve = false)
52
68
  addresses.map! do |address|
53
69
  address.is_a?(IPAddr) ? address : IPAddr.new(address.to_s)
@@ -56,17 +72,22 @@ module HTTPX
56
72
  # double emission check, but allow early resolution to work
57
73
  return if !early_resolve && connection.addresses && !addresses.intersect?(connection.addresses)
58
74
 
59
- log { "resolver: answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.origin.host}: #{addresses.inspect}" }
60
- if @pool && # if triggered by early resolve, pool may not be here yet
61
- !connection.io &&
62
- connection.options.ip_families.size > 1 &&
63
- family == Socket::AF_INET &&
64
- addresses.first.to_s != connection.origin.host.to_s
65
- log { "resolver: A response, applying resolution delay..." }
66
- @pool.after(0.05) do
67
- unless connection.state == :closed ||
68
- # double emission check
69
- (connection.addresses && addresses.intersect?(connection.addresses))
75
+ log do
76
+ "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
77
+ "answer #{connection.peer.host}: #{addresses.inspect} (early resolve: #{early_resolve})"
78
+ end
79
+
80
+ if !early_resolve && # do not apply resolution delay for non-dns name resolution
81
+ @current_selector && # just in case...
82
+ family == Socket::AF_INET && # resolution delay only applies to IPv4
83
+ !connection.io && # connection already has addresses and initiated/ended handshake
84
+ connection.options.ip_families.size > 1 && # no need to delay if not supporting dual stack IP
85
+ addresses.first.to_s != connection.peer.host.to_s # connection URL host is already the IP (early resolve included perhaps?)
86
+ log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }
87
+
88
+ @current_selector.after(0.05) do
89
+ # double emission check
90
+ unless connection.addresses && addresses.intersect?(connection.addresses)
70
91
  emit_resolved_connection(connection, addresses, early_resolve)
71
92
  end
72
93
  end
@@ -81,6 +102,8 @@ module HTTPX
81
102
  begin
82
103
  connection.addresses = addresses
83
104
 
105
+ return if connection.state == :closed
106
+
84
107
  emit(:resolve, connection)
85
108
  rescue StandardError => e
86
109
  if early_resolve
@@ -92,20 +115,22 @@ module HTTPX
92
115
  end
93
116
  end
94
117
 
95
- def early_resolve(connection, hostname: connection.origin.host)
118
+ def early_resolve(connection, hostname: connection.peer.host)
96
119
  addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
97
120
 
98
- return unless addresses
121
+ return false unless addresses
99
122
 
100
123
  addresses = addresses.select { |addr| addr.family == @family }
101
124
 
102
- return if addresses.empty?
125
+ return false if addresses.empty?
103
126
 
104
127
  emit_addresses(connection, @family, addresses, true)
128
+
129
+ true
105
130
  end
106
131
 
107
- def emit_resolve_error(connection, hostname = connection.origin.host, ex = nil)
108
- emit(:error, connection, resolve_error(hostname, ex))
132
+ def emit_resolve_error(connection, hostname = connection.peer.host, ex = nil)
133
+ emit_connection_error(connection, resolve_error(hostname, ex))
109
134
  end
110
135
 
111
136
  def resolve_error(hostname, ex = nil)
@@ -116,5 +141,25 @@ module HTTPX
116
141
  error.set_backtrace(ex ? ex.backtrace : caller)
117
142
  error
118
143
  end
144
+
145
+ def set_resolver_callbacks
146
+ on(:resolve, &method(:resolve_connection))
147
+ on(:error, &method(:emit_connection_error))
148
+ on(:close, &method(:close_resolver))
149
+ end
150
+
151
+ def resolve_connection(connection)
152
+ @current_session.__send__(:on_resolver_connection, connection, @current_selector)
153
+ end
154
+
155
+ def emit_connection_error(connection, error)
156
+ return connection.handle_connect_error(error) if connection.connecting?
157
+
158
+ connection.emit(:error, error)
159
+ end
160
+
161
+ def close_resolver(resolver)
162
+ @current_session.__send__(:on_resolver_close, resolver, @current_selector)
163
+ end
119
164
  end
120
165
  end
@@ -1,12 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
3
  require "resolv"
5
4
 
6
5
  module HTTPX
7
6
  class Resolver::System < Resolver::Resolver
8
7
  using URIExtensions
9
- extend Forwardable
10
8
 
11
9
  RESOLV_ERRORS = [Resolv::ResolvError,
12
10
  Resolv::DNS::Requester::RequestError,
@@ -24,8 +22,6 @@ module HTTPX
24
22
 
25
23
  attr_reader :state
26
24
 
27
- def_delegator :@connections, :empty?
28
-
29
25
  def initialize(options)
30
26
  super(nil, options)
31
27
  @resolver_options = @options.resolver_options
@@ -47,8 +43,12 @@ module HTTPX
47
43
  yield self
48
44
  end
49
45
 
50
- def connections
51
- EMPTY
46
+ def multi
47
+ self
48
+ end
49
+
50
+ def empty?
51
+ true
52
52
  end
53
53
 
54
54
  def close
@@ -84,7 +84,7 @@ module HTTPX
84
84
 
85
85
  return unless connection
86
86
 
87
- @timeouts[connection.origin.host].first
87
+ @timeouts[connection.peer.host].first
88
88
  end
89
89
 
90
90
  def <<(connection)
@@ -92,6 +92,11 @@ module HTTPX
92
92
  resolve
93
93
  end
94
94
 
95
+ def early_resolve(connection, **)
96
+ self << connection
97
+ true
98
+ end
99
+
95
100
  def handle_socket_timeout(interval)
96
101
  error = HTTPX::ResolveTimeoutError.new(interval, "timed out while waiting on select")
97
102
  error.set_backtrace(caller)
@@ -120,23 +125,26 @@ module HTTPX
120
125
  def consume
121
126
  return if @connections.empty?
122
127
 
123
- while @pipe_read.ready? && (event = @pipe_read.getbyte)
128
+ if @pipe_read.wait_readable
129
+ event = @pipe_read.getbyte
130
+
124
131
  case event
125
132
  when DONE
126
133
  *pair, addrs = @pipe_mutex.synchronize { @ips.pop }
127
134
  @queries.delete(pair)
135
+ _, connection = pair
136
+ @connections.delete(connection)
128
137
 
129
138
  family, connection = pair
130
- emit_addresses(connection, family, addrs)
139
+ catch(:coalesced) { emit_addresses(connection, family, addrs) }
131
140
  when ERROR
132
141
  *pair, error = @pipe_mutex.synchronize { @ips.pop }
133
142
  @queries.delete(pair)
143
+ @connections.delete(connection)
134
144
 
135
- family, connection = pair
136
- emit_resolve_error(connection, connection.origin.host, error)
145
+ _, connection = pair
146
+ emit_resolve_error(connection, connection.peer.host, error)
137
147
  end
138
-
139
- @connections.delete(connection) if @queries.empty?
140
148
  end
141
149
 
142
150
  return emit(:close, self) if @connections.empty?
@@ -148,9 +156,11 @@ module HTTPX
148
156
  raise Error, "no URI to resolve" unless connection
149
157
  return unless @queries.empty?
150
158
 
151
- hostname = connection.origin.host
159
+ hostname = connection.peer.host
152
160
  scheme = connection.origin.scheme
153
- log { "resolver: resolve IDN #{connection.origin.non_ascii_hostname} as #{hostname}" } if connection.origin.non_ascii_hostname
161
+ log do
162
+ "resolver: resolve IDN #{connection.peer.non_ascii_hostname} as #{hostname}"
163
+ end if connection.peer.non_ascii_hostname
154
164
 
155
165
  transition(:open)
156
166
 
@@ -164,7 +174,7 @@ module HTTPX
164
174
  def async_resolve(connection, hostname, scheme)
165
175
  families = connection.options.ip_families
166
176
  log { "resolver: query for #{hostname}" }
167
- timeouts = @timeouts[connection.origin.host]
177
+ timeouts = @timeouts[connection.peer.host]
168
178
  resolve_timeout = timeouts.first
169
179
 
170
180
  Thread.start do
@@ -210,5 +220,11 @@ module HTTPX
210
220
  def __addrinfo_resolve(host, scheme)
211
221
  Addrinfo.getaddrinfo(host, scheme, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
212
222
  end
223
+
224
+ def emit_connection_error(_, error)
225
+ throw(:resolve_error, error)
226
+ end
227
+
228
+ def close_resolver(resolver); end
213
229
  end
214
230
  end