httpx 1.4.1 → 1.4.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b8a0ae955506767cc7b2f0a8134dd920bc937f88aeba2f72f691489e4d2199ab
4
- data.tar.gz: 96503529f27fddf76b41250f3a7a0686c93d9d6c1091255275ed1d5b2beada11
3
+ metadata.gz: 3be0d06eb9b669cc4a1ba0818d39c76bdd4cdf41e496cb82efbb83b7c6c47241
4
+ data.tar.gz: bf778691f222095e080c83b16f71b3d9c8065db9f98f35498eeea317f013a686
5
5
  SHA512:
6
- metadata.gz: d16170323b9f5d016f496e848be6b6fb50046402ca644c2a016d05fe1af4a5b045a400b31530c8fa38f85a61351125062edce01bd04c975bc45c3205545aef71
7
- data.tar.gz: a800e8814449fcf2d3149aadc5f335f458f7dc7fa537d7c676d8460b6bfcfd1a9cc0a96f3350b9492846fea563fca15605f228ef539b39ef36985df05431d193
6
+ metadata.gz: 26045db79f85f7a136771bdca1852d93be6ad9c84a4285d26c7a97543e0209588fdc423276ea593c9ad26db960ec81a444326a01223e24a932cca8318f08e4ab
7
+ data.tar.gz: c280576f30f55590336c70c93ea63a4be1e9a8ac238318417b9c4227889518c04f2545d70c0dce74c4026465ad9a7e68f0a05ac6d9728a1035e8ba5c094036fc
data/README.md CHANGED
@@ -157,7 +157,6 @@ All Rubies greater or equal to 2.7, and always latest JRuby and Truffleruby.
157
157
 
158
158
  * Discuss your contribution in an issue
159
159
  * Fork it
160
- * Make your changes, add some tests
161
- * Ensure all tests pass (`docker-compose -f docker-compose.yml -f docker-compose-ruby-{RUBY_VERSION}.yml run httpx bundle exec rake test`)
160
+ * Make your changes, add some tests (follow the instructions from [here](test/README.md))
162
161
  * Open a Merge Request (that's Pull Request in Github-ish)
163
162
  * Wait for feedback
@@ -0,0 +1,20 @@
1
+ # 1.4.2
2
+
3
+ ## Bugfixes
4
+
5
+ * faraday: use default reason when none is matched by Net::HTTP::STATUS_CODES
6
+ * native resolver: keep sending DNS queries if the socket is available, to avoid busy loops on select
7
+ * native resolver fixes for Happy Eyeballs v2
8
+ * do not apply resolution delay if the IPv4 IP was not resolved via DNS
9
+ * ignore ALIAS if DNS response carries IP answers
10
+ * do not try to query for names already awaiting answer from the resolver
11
+ * make sure all types of errors are propagated to connections
12
+ * make sure next candidate is picked up if receiving NX_DOMAIN_NOT_FOUND error from resolver
13
+ * raise error happening before any request is flushed to respective connections (avoids loop on non-actionable selector termination).
14
+ * fix "NoMethodError: undefined method `after' for nil:NilClass", happening for requests flushed into persistent connections which errored, and were retried in a different connection before triggering the timeout callbacks from the previously-closed connection.
15
+
16
+
17
+ ## Chore
18
+
19
+ * Refactor of timers to allow for explicit and more performant single timer interval cancellation.
20
+ * default log message restructured to include info about process, thread and caller.
@@ -149,7 +149,7 @@ module Faraday
149
149
 
150
150
  module ResponseMethods
151
151
  def reason
152
- Net::HTTP::STATUS_CODES.fetch(@status)
152
+ Net::HTTP::STATUS_CODES.fetch(@status, "Non-Standard status code")
153
153
  end
154
154
  end
155
155
  end
@@ -4,7 +4,7 @@ module HTTPX
4
4
  module Callbacks
5
5
  def on(type, &action)
6
6
  callbacks(type) << action
7
- self
7
+ action
8
8
  end
9
9
 
10
10
  def once(type, &block)
@@ -12,10 +12,10 @@ module HTTPX
12
12
  block.call(*args, &callback)
13
13
  :delete
14
14
  end
15
- self
16
15
  end
17
16
 
18
17
  def emit(type, *args)
18
+ log { "emit #{type.inspect} callbacks" } if respond_to?(:log)
19
19
  callbacks(type).delete_if { |pr| :delete == pr.call(*args) } # rubocop:disable Style/YodaCondition
20
20
  end
21
21
 
@@ -125,7 +125,7 @@ module HTTPX
125
125
  end
126
126
 
127
127
  def handle_error(ex, request = nil)
128
- if ex.instance_of?(TimeoutError) && !@handshake_completed && @connection.state != :closed
128
+ if ex.is_a?(OperationTimeoutError) && !@handshake_completed && @connection.state != :closed
129
129
  @connection.goaway(:settings_timeout, "closing due to settings timeout")
130
130
  emit(:close_handshake)
131
131
  settings_ex = SettingsTimeoutError.new(ex.timeout, ex.message)
@@ -101,8 +101,6 @@ module HTTPX
101
101
  @inflight = 0
102
102
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
103
103
 
104
- @intervals = []
105
-
106
104
  self.addresses = @options.addresses if @options.addresses
107
105
  end
108
106
 
@@ -337,15 +335,7 @@ module HTTPX
337
335
  end
338
336
 
339
337
  def handle_socket_timeout(interval)
340
- @intervals.delete_if(&:elapsed?)
341
-
342
- unless @intervals.empty?
343
- # remove the intervals which will elapse
344
-
345
- return
346
- end
347
-
348
- error = HTTPX::TimeoutError.new(interval, "timed out while waiting on select")
338
+ error = OperationTimeoutError.new(interval, "timed out while waiting on select")
349
339
  error.set_backtrace(caller)
350
340
  on_error(error)
351
341
  end
@@ -379,18 +369,20 @@ module HTTPX
379
369
  force_reset(true)
380
370
  end
381
371
 
382
- private
383
-
384
- def connect
385
- transition(:open)
386
- end
387
-
388
372
  def disconnect
373
+ return unless @current_session && @current_selector
374
+
389
375
  emit(:close)
390
376
  @current_session = nil
391
377
  @current_selector = nil
392
378
  end
393
379
 
380
+ private
381
+
382
+ def connect
383
+ transition(:open)
384
+ end
385
+
394
386
  def consume
395
387
  return unless @io
396
388
 
@@ -485,6 +477,8 @@ module HTTPX
485
477
  end
486
478
  log(level: 3, color: :cyan) { "IO WRITE: #{siz} bytes..." }
487
479
  unless siz
480
+ @write_buffer.clear
481
+
488
482
  ex = EOFError.new("descriptor closed")
489
483
  ex.set_backtrace(caller)
490
484
  on_error(ex)
@@ -628,11 +622,15 @@ module HTTPX
628
622
  other_connection.merge(self)
629
623
  request.transition(:idle)
630
624
  other_connection.send(request)
631
- else
632
- response = ErrorResponse.new(request, ex)
633
- request.response = response
634
- request.emit(:response, response)
625
+ next
626
+ when OperationTimeoutError
627
+ # request level timeouts should take precedence
628
+ next unless request.active_timeouts.empty?
635
629
  end
630
+
631
+ response = ErrorResponse.new(request, ex)
632
+ request.response = response
633
+ request.emit(:response, response)
636
634
  end
637
635
  end
638
636
 
@@ -654,12 +652,14 @@ module HTTPX
654
652
  error.set_backtrace(e.backtrace)
655
653
  handle_connect_error(error) if connecting?
656
654
  @state = :closed
655
+ purge_after_closed
657
656
  disconnect
658
657
  rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
659
658
  # connect errors, exit gracefully
660
659
  handle_error(e)
661
660
  handle_connect_error(e) if connecting?
662
661
  @state = :closed
662
+ purge_after_closed
663
663
  disconnect
664
664
  end
665
665
 
@@ -812,7 +812,7 @@ module HTTPX
812
812
  end
813
813
 
814
814
  def on_error(error, request = nil)
815
- if error.instance_of?(TimeoutError)
815
+ if error.is_a?(OperationTimeoutError)
816
816
 
817
817
  # inactive connections do not contribute to the select loop, therefore
818
818
  # they should not fail due to such errors.
@@ -857,7 +857,7 @@ module HTTPX
857
857
 
858
858
  return if read_timeout.nil? || read_timeout.infinite?
859
859
 
860
- set_request_timeout(request, read_timeout, :done, :response) do
860
+ set_request_timeout(:read_timeout, request, read_timeout, :done, :response) do
861
861
  read_timeout_callback(request, read_timeout)
862
862
  end
863
863
  end
@@ -867,7 +867,7 @@ module HTTPX
867
867
 
868
868
  return if write_timeout.nil? || write_timeout.infinite?
869
869
 
870
- set_request_timeout(request, write_timeout, :headers, %i[done response]) do
870
+ set_request_timeout(:write_timeout, request, write_timeout, :headers, %i[done response]) do
871
871
  write_timeout_callback(request, write_timeout)
872
872
  end
873
873
  end
@@ -877,7 +877,7 @@ module HTTPX
877
877
 
878
878
  return if request_timeout.nil? || request_timeout.infinite?
879
879
 
880
- set_request_timeout(request, request_timeout, :headers, :complete) do
880
+ set_request_timeout(:request_timeout, request, request_timeout, :headers, :complete) do
881
881
  read_timeout_callback(request, request_timeout, RequestTimeoutError)
882
882
  end
883
883
  end
@@ -902,21 +902,18 @@ module HTTPX
902
902
  on_error(error, request)
903
903
  end
904
904
 
905
- def set_request_timeout(request, timeout, start_event, finish_events, &callback)
906
- request.once(start_event) do
907
- interval = @current_selector.after(timeout, callback)
905
+ def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
906
+ request.set_timeout_callback(start_event) do
907
+ timer = @current_selector.after(timeout, callback)
908
+ request.active_timeouts << label
908
909
 
909
910
  Array(finish_events).each do |event|
910
911
  # clean up request timeouts if the connection errors out
911
- request.once(event) do
912
- if @intervals.include?(interval)
913
- interval.delete(callback)
914
- @intervals.delete(interval) if interval.no_callbacks?
915
- end
912
+ request.set_timeout_callback(event) do
913
+ timer.cancel
914
+ request.active_timeouts.delete(label)
916
915
  end
917
916
  end
918
-
919
- @intervals << interval
920
917
  end
921
918
  end
922
919
 
data/lib/httpx/errors.rb CHANGED
@@ -77,6 +77,9 @@ module HTTPX
77
77
  # Error raised when there was a timeout while resolving a domain to an IP.
78
78
  class ResolveTimeoutError < TimeoutError; end
79
79
 
80
+ # Error raise when there was a timeout waiting for readiness of the socket the request is related to.
81
+ class OperationTimeoutError < TimeoutError; end
82
+
80
83
  # Error raised when there was an error while resolving a domain to an IP.
81
84
  class ResolveError < Error; end
82
85
 
@@ -22,7 +22,14 @@ module HTTPX
22
22
 
23
23
  return unless debug_stream
24
24
 
25
- message = (+"" << msg.call << "\n")
25
+ klass = self.class
26
+
27
+ until (class_name = klass.name)
28
+ klass = klass.superclass
29
+ end
30
+
31
+ message = +"(pid:#{Process.pid} tid:#{Thread.current.object_id}, self:#{class_name}##{object_id}) "
32
+ message << msg.call << "\n"
26
33
  message = "\e[#{COLORS[color]}m#{message}\e[0m" if color && debug_stream.respond_to?(:isatty) && debug_stream.isatty
27
34
  debug_stream << message
28
35
  end
@@ -25,6 +25,7 @@ module HTTPX
25
25
  class_eval(<<-MOD, __FILE__, __LINE__ + 1)
26
26
  def on_#{meth}(&blk) # def on_connection_opened(&blk)
27
27
  on(:#{meth}, &blk) # on(:connection_opened, &blk)
28
+ self # self
28
29
  end # end
29
30
  MOD
30
31
  end
@@ -36,6 +36,7 @@ module HTTPX
36
36
  class_eval(<<-MOD, __FILE__, __LINE__ + 1)
37
37
  def on_#{meth}(&blk) # def on_circuit_open(&blk)
38
38
  on(:#{meth}, &blk) # on(:circuit_open, &blk)
39
+ self # self
39
40
  end # end
40
41
  MOD
41
42
  end
@@ -84,7 +84,7 @@ module HTTPX
84
84
 
85
85
  return if expect_timeout.nil? || expect_timeout.infinite?
86
86
 
87
- set_request_timeout(request, expect_timeout, :expect, %i[body response]) do
87
+ set_request_timeout(:expect_timeout, request, expect_timeout, :expect, %i[body response]) do
88
88
  # expect timeout expired
89
89
  if request.state == :expect && !request.expects?
90
90
  Expect.no_expect_store << request.origin
data/lib/httpx/request.rb CHANGED
@@ -45,6 +45,8 @@ module HTTPX
45
45
 
46
46
  attr_writer :persistent
47
47
 
48
+ attr_reader :active_timeouts
49
+
48
50
  # will be +true+ when request body has been completely flushed.
49
51
  def_delegator :@body, :empty?
50
52
 
@@ -100,6 +102,7 @@ module HTTPX
100
102
  @response = nil
101
103
  @peer_address = nil
102
104
  @persistent = @options.persistent
105
+ @active_timeouts = []
103
106
  end
104
107
 
105
108
  # the read timeout defined for this requet.
@@ -245,8 +248,10 @@ module HTTPX
245
248
  @body.rewind
246
249
  @response = nil
247
250
  @drainer = nil
251
+ @active_timeouts.clear
248
252
  when :headers
249
253
  return unless @state == :idle
254
+
250
255
  when :body
251
256
  return unless @state == :headers ||
252
257
  @state == :expect
@@ -279,6 +284,15 @@ module HTTPX
279
284
  def expects?
280
285
  @headers["expect"] == "100-continue" && @informational_status == 100 && !@response
281
286
  end
287
+
288
+ def set_timeout_callback(event, &callback)
289
+ clb = once(event, &callback)
290
+
291
+ # reset timeout callbacks when requests get rerouted to a different connection
292
+ once(:idle) do
293
+ callbacks(event).delete(clb)
294
+ end
295
+ end
282
296
  end
283
297
  end
284
298
 
@@ -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,22 +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 do
67
- "resolver #{FAMILY_TYPES[@record_type]}: " \
68
- "failed resolving on nameserver #{@nameserver[@ns_index - 1]} (#{e.message})"
69
- end
70
- transition(:idle)
71
- @timeouts.clear
72
- else
73
- handle_error(e)
74
- end
75
- rescue NativeResolveError => e
76
- handle_error(e)
77
62
  end
78
63
 
79
64
  def interests
@@ -108,9 +93,7 @@ module HTTPX
108
93
  @timeouts.values_at(*hosts).reject(&:empty?).map(&:first).min
109
94
  end
110
95
 
111
- def handle_socket_timeout(interval)
112
- do_retry(interval)
113
- end
96
+ def handle_socket_timeout(interval); end
114
97
 
115
98
  private
116
99
 
@@ -123,32 +106,60 @@ module HTTPX
123
106
  end
124
107
 
125
108
  def consume
126
- dread if calculate_interests == :r
127
- do_retry
128
- 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?
129
138
  end
130
139
 
131
- def do_retry(loop_time = nil)
132
- return if @queries.empty? || !@start_timeout
140
+ def schedule_retry
141
+ h = @name
133
142
 
134
- loop_time ||= Utils.elapsed_time(@start_timeout)
143
+ return unless h
135
144
 
136
- query = @queries.first
145
+ connection = @queries[h]
137
146
 
138
- return unless query
147
+ timeouts = @timeouts[h]
148
+ timeout = timeouts.shift
139
149
 
140
- h, connection = query
141
- host = connection.peer.host
142
- timeout = (@timeouts[host][0] -= loop_time)
150
+ @timer = @current_selector.after(timeout) do
151
+ next unless @connections.include?(connection)
143
152
 
144
- return unless timeout <= 0
153
+ do_retry(h, connection, timeout)
154
+ end
155
+ end
145
156
 
146
- elapsed_after = @_timeouts[@_timeouts.size - @timeouts[host].size]
147
- @timeouts[host].shift
157
+ def do_retry(h, connection, interval)
158
+ timeouts = @timeouts[h]
148
159
 
149
- if !@timeouts[host].empty?
160
+ if !timeouts.empty?
150
161
  log do
151
- "resolver #{FAMILY_TYPES[@record_type]}: timeout after #{elapsed_after}s, retry (with #{@timeouts[host].first}s) #{host}..."
162
+ "resolver #{FAMILY_TYPES[@record_type]}: timeout after #{interval}s, retry (with #{timeouts.first}s) #{h}..."
152
163
  end
153
164
  # must downgrade to tcp AND retry on same host as last
154
165
  downgrade_socket
@@ -157,22 +168,28 @@ module HTTPX
157
168
  # try on the next nameserver
158
169
  @ns_index += 1
159
170
  log do
160
- "resolver #{FAMILY_TYPES[@record_type]}: failed resolving #{host} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)"
171
+ "resolver #{FAMILY_TYPES[@record_type]}: failed resolving #{h} on nameserver #{@nameserver[@ns_index - 1]} (timeout error)"
161
172
  end
162
173
  transition(:idle)
163
174
  @timeouts.clear
164
175
  resolve(connection, h)
165
176
  else
166
177
 
167
- @timeouts.delete(host)
178
+ @timeouts.delete(h)
168
179
  reset_hostname(h, reset_candidates: false)
169
180
 
170
- return unless @queries.empty?
181
+ unless @queries.empty?
182
+ resolve(connection)
183
+ return
184
+ end
171
185
 
172
186
  @connections.delete(connection)
187
+
188
+ host = connection.peer.host
189
+
173
190
  # This loop_time passed to the exception is bogus. Ideally we would pass the total
174
191
  # resolve timeout, including from the previous retries.
175
- ex = ResolveTimeoutError.new(loop_time, "Timed out while resolving #{connection.peer.host}")
192
+ ex = ResolveTimeoutError.new(interval, "Timed out while resolving #{host}")
176
193
  ex.set_backtrace(ex ? ex.backtrace : caller)
177
194
  emit_resolve_error(connection, host, ex)
178
195
 
@@ -225,7 +242,7 @@ module HTTPX
225
242
  parse(@read_buffer)
226
243
  end
227
244
 
228
- return if @state == :closed
245
+ return if @state == :closed || !@write_buffer.empty?
229
246
  end
230
247
  end
231
248
 
@@ -243,11 +260,15 @@ module HTTPX
243
260
 
244
261
  return unless siz.positive?
245
262
 
263
+ schedule_retry if @write_buffer.empty?
264
+
246
265
  return if @state == :closed
247
266
  end
248
267
  end
249
268
 
250
269
  def parse(buffer)
270
+ @timer.cancel
271
+
251
272
  code, result = Resolver.decode_dns_answer(buffer)
252
273
 
253
274
  case code
@@ -258,8 +279,10 @@ module HTTPX
258
279
  hostname, connection = @queries.first
259
280
  reset_hostname(hostname, reset_candidates: false)
260
281
 
261
- if @queries.value?(connection)
262
- resolve
282
+ other_candidate, _ = @queries.find { |_, conn| conn == connection }
283
+
284
+ if other_candidate
285
+ resolve(connection, other_candidate)
263
286
  else
264
287
  @connections.delete(connection)
265
288
  ex = NativeResolveError.new(connection, connection.peer.host, "name or service not known")
@@ -321,8 +344,10 @@ module HTTPX
321
344
  connection = @queries.delete(name)
322
345
  end
323
346
 
324
- if address.key?("alias") # CNAME
325
- 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"]
326
351
  # clean up intermediate queries
327
352
  @timeouts.delete(name) unless connection.peer.host == name
328
353
 
@@ -350,7 +375,11 @@ module HTTPX
350
375
  close_or_resolve
351
376
  end
352
377
 
353
- 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
+
354
383
  raise Error, "no URI to resolve" unless connection
355
384
 
356
385
  return unless @write_buffer.empty?
@@ -370,6 +399,9 @@ module HTTPX
370
399
  else
371
400
  @queries[hostname] = connection
372
401
  end
402
+
403
+ @name = hostname
404
+
373
405
  log { "resolver #{FAMILY_TYPES[@record_type]}: query for #{hostname}" }
374
406
  begin
375
407
  @write_buffer << encode_dns_query(hostname)
@@ -458,6 +490,7 @@ module HTTPX
458
490
  # these errors may happen during TCP handshake
459
491
  # treat them as resolve errors.
460
492
  handle_error(e)
493
+ emit(:close, self)
461
494
  end
462
495
 
463
496
  def handle_error(error)
@@ -472,13 +505,15 @@ module HTTPX
472
505
  @connections.delete(connection)
473
506
  emit_resolve_error(connection, host, error)
474
507
  end
508
+
509
+ while (connection = @connections.shift)
510
+ emit_resolve_error(connection, host, error)
511
+ end
475
512
  end
476
- close_or_resolve
477
513
  end
478
514
 
479
515
  def reset_hostname(hostname, connection: @queries.delete(hostname), reset_candidates: true)
480
516
  @timeouts.delete(hostname)
481
- @timeouts.delete(hostname)
482
517
 
483
518
  return unless connection && reset_candidates
484
519
 
@@ -490,7 +525,10 @@ module HTTPX
490
525
  end
491
526
 
492
527
  def close_or_resolve
493
- if @connections.empty?
528
+ # drop already closed connections
529
+ @connections.shift until @connections.empty? || @connections.first.state != :closed
530
+
531
+ if (@connections - @queries.values).empty?
494
532
  emit(:close, self)
495
533
  else
496
534
  resolve
@@ -74,14 +74,15 @@ module HTTPX
74
74
 
75
75
  log do
76
76
  "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: " \
77
- "answer #{FAMILY_TYPES[RECORD_TYPES[family]]} #{connection.peer.host}: #{addresses.inspect}"
77
+ "answer #{connection.peer.host}: #{addresses.inspect} (early resolve: #{early_resolve})"
78
78
  end
79
79
 
80
- if @current_selector && # if triggered by early resolve, session may not be here yet
81
- !connection.io &&
82
- connection.options.ip_families.size > 1 &&
83
- family == Socket::AF_INET &&
84
- addresses.first.to_s != connection.peer.host.to_s
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?)
85
86
  log { "resolver #{FAMILY_TYPES[RECORD_TYPES[family]]}: applying resolution delay..." }
86
87
 
87
88
  @current_selector.after(0.05) do
@@ -19,6 +19,7 @@ module HTTPX
19
19
  def initialize
20
20
  @timers = Timers.new
21
21
  @selectables = []
22
+ @is_timer_interval = false
22
23
  end
23
24
 
24
25
  def each(&blk)
@@ -43,7 +44,11 @@ module HTTPX
43
44
  rescue StandardError => e
44
45
  emit_error(e)
45
46
  rescue Exception # rubocop:disable Lint/RescueException
46
- each_connection(&:force_reset)
47
+ each_connection do |conn|
48
+ conn.force_reset
49
+ conn.disconnect
50
+ end
51
+
47
52
  raise
48
53
  end
49
54
 
@@ -125,24 +130,22 @@ module HTTPX
125
130
  # first, we group IOs based on interest type. On call to #interests however,
126
131
  # things might already happen, and new IOs might be registered, so we might
127
132
  # have to start all over again. We do this until we group all selectables
128
- begin
129
- @selectables.delete_if do |io|
130
- interests = io.interests
133
+ @selectables.delete_if do |io|
134
+ interests = io.interests
131
135
 
132
- (r ||= []) << io if READABLE.include?(interests)
133
- (w ||= []) << io if WRITABLE.include?(interests)
136
+ (r ||= []) << io if READABLE.include?(interests)
137
+ (w ||= []) << io if WRITABLE.include?(interests)
134
138
 
135
- io.state == :closed
136
- end
139
+ io.state == :closed
140
+ end
137
141
 
138
- # TODO: what to do if there are no selectables?
142
+ # TODO: what to do if there are no selectables?
139
143
 
140
- readers, writers = IO.select(r, w, nil, interval)
144
+ readers, writers = IO.select(r, w, nil, interval)
141
145
 
142
- if readers.nil? && writers.nil? && interval
143
- [*r, *w].each { |io| io.handle_socket_timeout(interval) }
144
- return
145
- end
146
+ if readers.nil? && writers.nil? && interval
147
+ [*r, *w].each { |io| io.handle_socket_timeout(interval) }
148
+ return
146
149
  end
147
150
 
148
151
  if writers
@@ -174,7 +177,7 @@ module HTTPX
174
177
  end
175
178
 
176
179
  unless result || interval.nil?
177
- io.handle_socket_timeout(interval)
180
+ io.handle_socket_timeout(interval) unless @is_timer_interval
178
181
  return
179
182
  end
180
183
  # raise TimeoutError.new(interval, "timed out while waiting on select")
@@ -186,10 +189,21 @@ module HTTPX
186
189
  end
187
190
 
188
191
  def next_timeout
189
- [
190
- @timers.wait_interval,
191
- @selectables.filter_map(&:timeout).min,
192
- ].compact.min
192
+ @is_timer_interval = false
193
+
194
+ timer_interval = @timers.wait_interval
195
+
196
+ connection_interval = @selectables.filter_map(&:timeout).min
197
+
198
+ return connection_interval unless timer_interval
199
+
200
+ if connection_interval.nil? || timer_interval <= connection_interval
201
+ @is_timer_interval = true
202
+
203
+ return timer_interval
204
+ end
205
+
206
+ connection_interval
193
207
  end
194
208
 
195
209
  def emit_error(e)
data/lib/httpx/session.rb CHANGED
@@ -240,11 +240,9 @@ module HTTPX
240
240
  end
241
241
  return unless error && error.is_a?(Exception)
242
242
 
243
- if error.is_a?(Error)
244
- request.emit(:response, ErrorResponse.new(request, error))
245
- else
246
- raise error if selector.empty?
247
- end
243
+ raise error unless error.is_a?(Error)
244
+
245
+ request.emit(:response, ErrorResponse.new(request, error))
248
246
  end
249
247
 
250
248
  # returns a set of HTTPX::Request objects built from the given +args+ and +options+.
data/lib/httpx/timers.rb CHANGED
@@ -26,7 +26,7 @@ module HTTPX
26
26
 
27
27
  @next_interval_at = nil
28
28
 
29
- interval
29
+ Timer.new(interval, callback)
30
30
  end
31
31
 
32
32
  def wait_interval
@@ -48,6 +48,17 @@ module HTTPX
48
48
  @next_interval_at = nil if @intervals.empty?
49
49
  end
50
50
 
51
+ class Timer
52
+ def initialize(interval, callback)
53
+ @interval = interval
54
+ @callback = callback
55
+ end
56
+
57
+ def cancel
58
+ @interval.delete(@callback)
59
+ end
60
+ end
61
+
51
62
  class Interval
52
63
  include Comparable
53
64
 
@@ -63,6 +74,10 @@ module HTTPX
63
74
  @on_empty = blk
64
75
  end
65
76
 
77
+ def cancel
78
+ @on_empty.call
79
+ end
80
+
66
81
  def <=>(other)
67
82
  @interval <=> other.interval
68
83
  end
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "1.4.1"
4
+ VERSION = "1.4.2"
5
5
  end
data/sig/callbacks.rbs CHANGED
@@ -4,8 +4,8 @@ module HTTPX
4
4
  end
5
5
 
6
6
  module Callbacks
7
- def on: (Symbol) { (*untyped) -> void } -> self
8
- def once: (Symbol) { (*untyped) -> void } -> self
7
+ def on: (Symbol) { (*untyped) -> void } -> ^(*untyped) -> void
8
+ def once: (Symbol) { (*untyped) -> void } -> ^(*untyped) -> void
9
9
  def emit: (Symbol, *untyped) -> void
10
10
 
11
11
  def callbacks_for?: (Symbol) -> bool
data/sig/connection.rbs CHANGED
@@ -43,7 +43,6 @@ module HTTPX
43
43
  @parser: Object & _Parser
44
44
  @connected_at: Float
45
45
  @response_received_at: Float
46
- @intervals: Array[Timers::Interval]
47
46
  @exhausted: bool
48
47
  @cloned: bool
49
48
  @coalesced_connection: instance?
@@ -111,6 +110,8 @@ module HTTPX
111
110
 
112
111
  def handle_connect_error: (StandardError error) -> void
113
112
 
113
+ def disconnect: () -> void
114
+
114
115
  private
115
116
 
116
117
  def initialize: (http_uri uri, Options options) -> void
@@ -119,8 +120,6 @@ module HTTPX
119
120
 
120
121
  def connect: () -> void
121
122
 
122
- def disconnect: () -> void
123
-
124
123
  def exhausted?: () -> boolish
125
124
 
126
125
  def consume: () -> void
@@ -163,7 +162,7 @@ module HTTPX
163
162
 
164
163
  def read_timeout_callback: (Request request, Numeric read_timeout, ?singleton(RequestTimeoutError) error_type) -> void
165
164
 
166
- def set_request_timeout: (Request request, Numeric timeout, Symbol start_event, Symbol | Array[Symbol] finish_events) { () -> void } -> void
165
+ def set_request_timeout: (Symbol label, Request request, Numeric timeout, Symbol start_event, Symbol | Array[Symbol] finish_events) { () -> void } -> void
167
166
 
168
167
  def self.parser_type: (String protocol) -> (singleton(HTTP1) | singleton(HTTP2))
169
168
  end
data/sig/errors.rbs CHANGED
@@ -45,6 +45,9 @@ module HTTPX
45
45
  class WriteTimeoutError < RequestTimeoutError
46
46
  end
47
47
 
48
+ class OperationTimeoutError < TimeoutError
49
+ end
50
+
48
51
  class ResolveError < Error
49
52
  end
50
53
 
data/sig/request.rbs CHANGED
@@ -14,6 +14,7 @@ module HTTPX
14
14
  attr_reader options: Options
15
15
  attr_reader response: response?
16
16
  attr_reader drain_error: StandardError?
17
+ attr_reader active_timeouts: Array[Symbol]
17
18
 
18
19
  attr_accessor peer_address: ipaddr?
19
20
 
@@ -63,6 +64,8 @@ module HTTPX
63
64
 
64
65
  def request_timeout: () -> Numeric?
65
66
 
67
+ def set_timeout_callback: (Symbol event) { (*untyped) -> void } -> void
68
+
66
69
  private
67
70
 
68
71
  def initialize_body: (Options options) -> Transcoder::_Encoder?
@@ -21,6 +21,7 @@ module HTTPX
21
21
  @write_buffer: Buffer
22
22
  @large_packet: Buffer?
23
23
  @io: UDP | TCP
24
+ @name: String?
24
25
 
25
26
  attr_reader state: Symbol
26
27
 
@@ -42,7 +43,9 @@ module HTTPX
42
43
 
43
44
  def consume: () -> void
44
45
 
45
- def do_retry: (?Numeric? loop_time) -> void
46
+ def schedule_retry: () -> void
47
+
48
+ def do_retry: (String host, Connection connection, Numeric interval) -> void
46
49
 
47
50
  def dread: (Integer) -> void
48
51
  | () -> void
data/sig/selector.rbs CHANGED
@@ -10,6 +10,7 @@ module HTTPX
10
10
  @timers: Timers
11
11
 
12
12
  @selectables: Array[selectable]
13
+ @is_timer_interval: bool
13
14
 
14
15
  def next_tick: () -> void
15
16
 
data/sig/timers.rbs CHANGED
@@ -1,10 +1,12 @@
1
1
  module HTTPX
2
2
  class Timers
3
+ type callback = ^() -> void
4
+
3
5
  @intervals: Array[Interval]
4
6
  @next_interval_at: Float
5
7
 
6
- def after: (Numeric interval_in_secs, ^() -> void) -> Interval
7
- | (Numeric interval_in_secs) { () -> void } -> Interval
8
+ def after: (Numeric interval_in_secs, ^() -> void) -> Timer
9
+ | (Numeric interval_in_secs) { () -> void } -> Timer
8
10
 
9
11
  def wait_interval: () -> Numeric?
10
12
 
@@ -15,8 +17,6 @@ module HTTPX
15
17
  class Interval
16
18
  include Comparable
17
19
 
18
- type callback = ^() -> void
19
-
20
20
  attr_reader interval: Numeric
21
21
 
22
22
  @callbacks: Array[callback]
@@ -25,6 +25,8 @@ module HTTPX
25
25
 
26
26
  def on_empty: () { () -> void } -> void
27
27
 
28
+ def cancel: () -> void
29
+
28
30
  def to_f: () -> Float
29
31
 
30
32
  def <<: (callback) -> void
@@ -41,5 +43,14 @@ module HTTPX
41
43
 
42
44
  def initialize: (Numeric interval) -> void
43
45
  end
46
+
47
+ class Timer
48
+ @interval: Interval
49
+ @callback: callback
50
+
51
+ def initialize: (Interval interval, callback callback) -> void
52
+
53
+ def cancel: () -> void
54
+ end
44
55
  end
45
56
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpx
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-18 00:00:00.000000000 Z
11
+ date: 2025-03-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http-2
@@ -151,6 +151,7 @@ extra_rdoc_files:
151
151
  - doc/release_notes/1_3_4.md
152
152
  - doc/release_notes/1_4_0.md
153
153
  - doc/release_notes/1_4_1.md
154
+ - doc/release_notes/1_4_2.md
154
155
  files:
155
156
  - LICENSE.txt
156
157
  - README.md
@@ -273,6 +274,7 @@ files:
273
274
  - doc/release_notes/1_3_4.md
274
275
  - doc/release_notes/1_4_0.md
275
276
  - doc/release_notes/1_4_1.md
277
+ - doc/release_notes/1_4_2.md
276
278
  - lib/httpx.rb
277
279
  - lib/httpx/adapters/datadog.rb
278
280
  - lib/httpx/adapters/faraday.rb
@@ -493,7 +495,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
493
495
  - !ruby/object:Gem::Version
494
496
  version: '0'
495
497
  requirements: []
496
- rubygems_version: 3.5.3
498
+ rubygems_version: 3.5.22
497
499
  signing_key:
498
500
  specification_version: 4
499
501
  summary: HTTPX, to the future, and beyond