httpx 1.9.0 → 1.9.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_8_1.md +1 -1
  3. data/doc/release_notes/1_8_2.md +4 -4
  4. data/doc/release_notes/1_8_3.md +3 -5
  5. data/doc/release_notes/1_8_4.md +1 -1
  6. data/doc/release_notes/1_9_0.md +7 -7
  7. data/doc/release_notes/1_9_1.md +32 -0
  8. data/lib/httpx/adapters/faraday.rb +1 -1
  9. data/lib/httpx/adapters/webmock.rb +14 -1
  10. data/lib/httpx/connection/http1.rb +24 -17
  11. data/lib/httpx/connection/http2.rb +108 -29
  12. data/lib/httpx/connection.rb +158 -162
  13. data/lib/httpx/io/tcp.rb +24 -7
  14. data/lib/httpx/plugins/aws_sigv4.rb +0 -1
  15. data/lib/httpx/plugins/cache.rb +1 -1
  16. data/lib/httpx/plugins/callbacks.rb +1 -0
  17. data/lib/httpx/plugins/content_digest.rb +3 -5
  18. data/lib/httpx/plugins/expect.rb +8 -1
  19. data/lib/httpx/plugins/fiber_concurrency.rb +56 -19
  20. data/lib/httpx/plugins/h2c.rb +12 -11
  21. data/lib/httpx/plugins/hedging.rb +2 -2
  22. data/lib/httpx/plugins/ntlm_auth.rb +4 -3
  23. data/lib/httpx/plugins/ntlm_v2_auth.rb +4 -3
  24. data/lib/httpx/plugins/oauth.rb +1 -1
  25. data/lib/httpx/plugins/proxy/http.rb +23 -16
  26. data/lib/httpx/plugins/proxy/socks5.rb +4 -4
  27. data/lib/httpx/plugins/proxy.rb +8 -15
  28. data/lib/httpx/plugins/query.rb +1 -0
  29. data/lib/httpx/plugins/stream.rb +12 -10
  30. data/lib/httpx/plugins/stream_bidi.rb +4 -0
  31. data/lib/httpx/plugins/tracing.rb +62 -21
  32. data/lib/httpx/plugins/upgrade/h2.rb +2 -2
  33. data/lib/httpx/request.rb +1 -1
  34. data/lib/httpx/resolver/https.rb +3 -2
  35. data/lib/httpx/resolver/native.rb +19 -9
  36. data/lib/httpx/resolver/resolver.rb +2 -2
  37. data/lib/httpx/resolver/system.rb +4 -0
  38. data/lib/httpx/resolver.rb +5 -5
  39. data/lib/httpx/response.rb +5 -1
  40. data/lib/httpx/selector.rb +108 -7
  41. data/lib/httpx/transcoder/json.rb +1 -1
  42. data/lib/httpx/version.rb +1 -1
  43. data/lib/httpx.rb +1 -0
  44. data/sig/chainable.rbs +2 -1
  45. data/sig/connection/http1.rbs +4 -2
  46. data/sig/connection/http2.rbs +8 -2
  47. data/sig/connection.rbs +11 -6
  48. data/sig/io/tcp.rbs +5 -0
  49. data/sig/plugins/expect.rbs +6 -0
  50. data/sig/plugins/fiber_concurrency.rbs +10 -1
  51. data/sig/plugins/proxy/http.rbs +3 -1
  52. data/sig/plugins/stream_bidi.rbs +0 -2
  53. data/sig/plugins/tracing.rbs +10 -2
  54. data/sig/resolver/https.rbs +2 -0
  55. data/sig/resolver/native.rbs +3 -0
  56. data/sig/resolver/resolver.rbs +1 -1
  57. data/sig/resolver/system.rbs +2 -0
  58. data/sig/resolver.rbs +1 -1
  59. data/sig/selector.rbs +15 -1
  60. data/sig/session.rbs +1 -1
  61. metadata +3 -1
@@ -39,9 +39,7 @@ module HTTPX
39
39
  attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session, :sibling,
40
40
  :read_buffer, :write_buffer
41
41
 
42
- attr_writer :current_selector
43
-
44
- attr_accessor :current_session, :family
42
+ attr_accessor :current_selector, :current_session, :family
45
43
 
46
44
  protected :ssl_session, :sibling, :read_buffer, :write_buffer
47
45
 
@@ -65,7 +63,6 @@ module HTTPX
65
63
  @read_buffer = Buffer.new(@options.buffer_size)
66
64
  @write_buffer = Buffer.new(@options.buffer_size)
67
65
  @pending = []
68
- @inflight = 0
69
66
  @keep_alive_timeout = @options.timeout.keep_alive_timeout
70
67
  @no_more_requests_counter = 0
71
68
 
@@ -95,7 +92,6 @@ module HTTPX
95
92
  @origins = orig.origins.dup
96
93
  @read_buffer = orig.read_buffer.dup
97
94
  @write_buffer = orig.write_buffer.dup
98
- @inflight = 0
99
95
  @pending = []
100
96
  transition(:idle)
101
97
 
@@ -134,7 +130,7 @@ module HTTPX
134
130
  end
135
131
 
136
132
  def match?(uri, options)
137
- return false if !used? && (@state == :closing || @state == :closed)
133
+ return false if @state == :closing || (!used? && @state == :closed)
138
134
 
139
135
  @origins.include?(uri.origin) &&
140
136
  # if there is more than one origin to match, it means that this connection
@@ -148,6 +144,9 @@ module HTTPX
148
144
  def mergeable?(connection)
149
145
  return false if @state == :closing || @state == :closed || !@io
150
146
 
147
+ # do not coalesce with HEv2 sibling
148
+ return false if @sibling.equal?(connection)
149
+
151
150
  return false unless connection.addresses
152
151
 
153
152
  (
@@ -198,24 +197,13 @@ module HTTPX
198
197
  # returns the number of pending requests both in the connection as well as parser
199
198
  # pending queues
200
199
  def pending_count
201
- count = @pending.size
202
- count += @parser.pending.size if @parser.respond_to?(:pending)
203
- count
200
+ @pending.size
204
201
  end
205
202
 
206
203
  def purge_pending(&block)
207
- if @parser
208
- pending = @parser.pending
209
- @inflight -= pending.size
210
- pending.reject! do |req|
211
- block.call(req)
212
- true
213
- end
214
- end
215
- @pending.reject! do |req|
216
- block.call(req)
217
- true
218
- end
204
+ pending = @pending.dup
205
+ @pending.clear
206
+ pending.each(&block)
219
207
  end
220
208
 
221
209
  def io_connected?
@@ -228,6 +216,11 @@ module HTTPX
228
216
  @state == :idle
229
217
  end
230
218
 
219
+ # whether the connection can be removed from the selector.
220
+ def closed_for_select?
221
+ @state == :closed
222
+ end
223
+
231
224
  def inflight?
232
225
  @parser && (
233
226
  # parser may be dealing with other requests (possibly started from a different fiber)
@@ -272,7 +265,7 @@ module HTTPX
272
265
 
273
266
  consume
274
267
  when :closed
275
- return if no_more_requests?
268
+ return if no_requests_to_handle?
276
269
 
277
270
  # there are pending requests to send, restart the state machine.
278
271
  idling
@@ -283,8 +276,18 @@ module HTTPX
283
276
 
284
277
  call
285
278
  when :closing
279
+ # if bytes are available, that's the client GOAWAY frame. Otherwise, the peer closed it.
280
+ sending_goaway = !@write_buffer.empty?
281
+
286
282
  consume
287
- transition(:closed)
283
+
284
+ if sending_goaway
285
+ # parser may have inflight streams which need terminating.
286
+ @parser&.close
287
+ transition(:closed)
288
+ elsif @state == :closing
289
+ return
290
+ end
288
291
 
289
292
  # @fiber-switch-guard
290
293
  # fiber may have switch while closing @io.
@@ -329,6 +332,7 @@ module HTTPX
329
332
 
330
333
  # @fiber-switch-guard
331
334
  if @io.can_disconnect? && @pending.empty?
335
+ @state = :closed
332
336
  disconnect
333
337
  return
334
338
  end
@@ -348,7 +352,7 @@ module HTTPX
348
352
  if delete_pending
349
353
  @pending.clear
350
354
  elsif (parser = @parser)
351
- enqueue_pending_requests_from_parser(parser)
355
+ parser.reset_requests if parser.respond_to?(:reset_requests)
352
356
  return unless @pending.empty?
353
357
  end
354
358
 
@@ -365,7 +369,7 @@ module HTTPX
365
369
  end
366
370
 
367
371
  def reset
368
- return if @state == :closing || @state == :closed
372
+ return if @state == :closed
369
373
 
370
374
  # do not reset a connection which may have restarted back to :idle, such when the parser resets
371
375
  # (example: HTTP/1 parser disabling pipelining)
@@ -376,6 +380,8 @@ module HTTPX
376
380
  parser = @parser
377
381
 
378
382
  if parser && parser.respond_to?(:max_concurrent_requests)
383
+ # @type var parser: HTTP1
384
+
379
385
  # if connection being reset has at some downgraded the number of concurrent
380
386
  # requests, such as in the case where an attempt to use HTTP/1 pipelining failed,
381
387
  # keep that information around.
@@ -416,9 +422,14 @@ module HTTPX
416
422
  def timeout
417
423
  return if @state == :closed || @state == :inactive
418
424
 
419
- return @timeout if @timeout
425
+ timeout = @timeout || @options.timeout.operation_timeout
426
+
427
+ return timeout unless connecting? && (attempt_timeout = connect_attempt_time_left)
420
428
 
421
- @options.timeout.operation_timeout
429
+ # the attempt may already be overdue
430
+ attempt_timeout = 0 unless attempt_timeout.positive?
431
+
432
+ timeout && timeout < attempt_timeout ? timeout : attempt_timeout
422
433
  end
423
434
 
424
435
  def idling
@@ -430,7 +441,7 @@ module HTTPX
430
441
  transition(:idle)
431
442
  return unless @parser
432
443
 
433
- enqueue_pending_requests_from_parser(@parser)
444
+ @parser.reset_requests if @parser.respond_to?(:reset_requests)
434
445
  @parser = nil
435
446
  end
436
447
 
@@ -439,6 +450,9 @@ module HTTPX
439
450
  end
440
451
 
441
452
  def deactivate
453
+ # safety net so that connection state does not corrupt.
454
+ clear_pending_requests
455
+
442
456
  transition(:inactive)
443
457
  end
444
458
 
@@ -480,6 +494,9 @@ module HTTPX
480
494
 
481
495
  @current_session = @current_selector = nil
482
496
 
497
+ # NOTE: don't change state of the connection after this call!
498
+ # the connection is checked back into the pool, and may be checked out by
499
+ # another thread before the next statement.
483
500
  current_session.deselect_connection(self, current_selector, @cloned)
484
501
  end
485
502
 
@@ -507,16 +524,33 @@ module HTTPX
507
524
  # they should not fail due to such errors.
508
525
  return if @state == :inactive
509
526
 
510
- if (current_timeout = @timeout || timeout)
527
+ attempt_timeout = connect_attempt_time_left if connecting?
528
+
529
+ current_timeout = @timeout
530
+ current_timeout ||= @options.timeout.operation_timeout unless @state == :closed
531
+
532
+ if current_timeout
511
533
  # if @timeout isn't set, it's not a completion-based timeout, i.e.
512
534
  # operation timeout; in such a case, we have to reset it once it piggybacks.
513
535
  @reset_timeout_on_call ||= @timeout.nil?
514
536
 
515
537
  @timeout = current_timeout - error.timeout
516
- return unless @timeout <= 0
538
+
539
+ if @timeout.positive?
540
+ # if this was a connect timeout error and there are more addresses to try.
541
+ connect_next(error) if attempt_timeout && !attempt_timeout.positive?
542
+ return
543
+ end
517
544
 
518
545
  @timeout = nil
519
546
  @reset_timeout_on_call = false
547
+ elsif attempt_timeout
548
+ # when request timeouts are set instead.
549
+ connect_next(error) unless attempt_timeout.positive?
550
+ return
551
+ else
552
+ # the select interval came from another selectable, as this one has no timeout set.
553
+ return
520
554
  end
521
555
 
522
556
  error = error.to_connection_error if connecting?
@@ -537,6 +571,21 @@ module HTTPX
537
571
 
538
572
  private
539
573
 
574
+ # the time left until the attempt to connect to the current address should be abandoned.
575
+ def connect_attempt_time_left
576
+ io = @io
577
+
578
+ return unless io.respond_to?(:connect_attempt_time_left)
579
+
580
+ io.connect_attempt_time_left
581
+ end
582
+
583
+ def connect_next(error)
584
+ @io.next_address!(error)
585
+
586
+ connect
587
+ end
588
+
540
589
  def try_connect
541
590
  emit(:connecting)
542
591
  connect
@@ -565,7 +614,6 @@ module HTTPX
565
614
  # * the number of inflight requests
566
615
  # * whether the write buffer has bytes (i.e. for close handshake)
567
616
  if no_more_requests? && @write_buffer.empty?
568
- no_more_requests_loop_check if @parser && @parser.pending.any?
569
617
 
570
618
  # terminate if an altsvc connection has been established
571
619
  terminate if @altsvc_connection
@@ -608,21 +656,19 @@ module HTTPX
608
656
  parser << @read_buffer.to_s
609
657
 
610
658
  # continue reading if possible.
611
- break if interests == :w && !epiped
612
-
613
- # exit the read loop if connection is preparing to be closed
614
- break if @state == :closing || @state == :closed
659
+ break if (interests == :w && !epiped) ||
660
+ # exit the read loop if connection is closed
661
+ @state == :closed
615
662
 
616
663
  # exit #consume altogether if all outstanding requests have been dealt with
617
664
  if no_more_requests? && @write_buffer.empty? # rubocop:disable Style/Next
618
- no_more_requests_loop_check if @parser && @parser.pending.any?
619
665
 
620
666
  # terminate if an altsvc connection has been established
621
667
  terminate if @altsvc_connection
622
668
 
623
669
  return
624
670
  end
625
- end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
671
+ end unless ((ints = interests).nil? || ints == :w) && !epiped
626
672
 
627
673
  #
628
674
  # tight write loop.
@@ -633,7 +679,7 @@ module HTTPX
633
679
  # buffer has been drained, mark and exit the write loop.
634
680
  if @write_buffer.empty?
635
681
  # we only mark as drained on the first loop
636
- write_drained = write_drained.nil? && @inflight.positive?
682
+ write_drained = write_drained.nil? && !@parser&.no_more_requests?
637
683
 
638
684
  break
639
685
  end
@@ -668,11 +714,12 @@ module HTTPX
668
714
  end
669
715
 
670
716
  # exit write loop if marked to consume from peer, or is closing.
671
- break if interests == :r || @state == :closing || @state == :closed
717
+ break if (ints = interests) == :r || @state == :closed
672
718
 
673
719
  write_drained = false
674
720
  end unless (ints = interests) == :r
675
721
 
722
+ # fill the write buffer with more requests
676
723
  send_pending if @state == :open
677
724
 
678
725
  # return if socket is drained
@@ -689,8 +736,9 @@ module HTTPX
689
736
  end
690
737
 
691
738
  def send_pending
692
- while !@write_buffer.full? && (request = @pending.shift)
693
- send_request_to_parser(request)
739
+ while (request = @pending.shift)
740
+ # request may have been rerouted to the pending list
741
+ break unless send_request_to_parser(request, true)
694
742
  end
695
743
  end
696
744
 
@@ -698,44 +746,29 @@ module HTTPX
698
746
  @parser ||= build_parser
699
747
  end
700
748
 
701
- def send_request_to_parser(request)
702
- @inflight += 1
703
- request.peer_address = @io.ip && @io.ip.address
704
- set_request_timeouts(request)
705
-
706
- parser.send(request)
749
+ def send_request_to_parser(request, head = false)
750
+ request.peer_address = @io&.ip&.address
707
751
 
708
- return unless @state == :inactive
709
-
710
- transition(:active)
711
- # mark request as ping, as this inactive connection may have been
712
- # closed by the server, and we don't want that to influence retry
713
- # bookkeeping.
714
- request.ping!
715
- end
716
-
717
- def enqueue_pending_requests_from_parser(parser)
718
- parser.reset_requests # move sequential requests back to pending queue.
719
- parser_pending_requests = parser.pending
720
-
721
- return if parser_pending_requests.empty?
722
-
723
- # the connection will be reused, so parser requests must come
724
- # back to the pending list before the parser is reset.
725
- @inflight -= parser_pending_requests.size
726
- @pending.unshift(*parser_pending_requests)
752
+ if @state == :inactive
753
+ transition(:active)
754
+ # mark request as ping, as this inactive connection may have been
755
+ # closed by the server, and we don't want that to influence retry
756
+ # bookkeeping.
757
+ request.ping!
758
+ end
727
759
 
728
- parser.pending.clear
760
+ parser.send(request, head)
729
761
  end
730
762
 
731
763
  def build_parser(protocol = @io.protocol)
732
- parser = parser_type(protocol).new(@write_buffer, @options)
764
+ parser = parser_type(protocol).new(@write_buffer, @pending, @options)
733
765
  set_parser_callbacks(parser)
734
766
  parser.max_concurrent_requests = @max_concurrent_requests if @max_concurrent_requests && parser.respond_to?(:max_concurrent_requests=)
735
767
  parser
736
768
  end
737
769
 
738
770
  def set_parser_callbacks(parser)
771
+ parser.on(:request_accepted, &method(:set_request_timeouts))
739
772
  parser.on(:response) do |request, response|
740
773
  AltSvc.emit(request, response) do |alt_origin, origin, alt_params|
741
774
  build_altsvc_connection(alt_origin, origin, alt_params)
@@ -743,7 +776,6 @@ module HTTPX
743
776
  @previously_exhausted_with_error = false
744
777
  @response_received_at = Utils.now
745
778
  @no_more_requests_counter = 0
746
- @inflight -= 1
747
779
  response.finish!
748
780
  request.emit_response(response)
749
781
  end
@@ -757,7 +789,7 @@ module HTTPX
757
789
  request.emit(:promise, parser, stream)
758
790
  end
759
791
  parser.on(:exhausted) do |error|
760
- enqueue_pending_requests_from_parser(parser)
792
+ parser.reset_requests
761
793
 
762
794
  if error
763
795
  if @previously_exhausted_with_error
@@ -786,6 +818,13 @@ module HTTPX
786
818
  parser.on(:origin) do |origin|
787
819
  @origins |= [origin]
788
820
  end
821
+ parser.on(:closing) do
822
+ # this happens if the peer server sent a GOAWAY frame while there are inflight
823
+ # streams; moving to :closing ensures that new requests won't be sent to this
824
+ # connection, while keeping it around to complete unfinished work
825
+ reset_ping_timer
826
+ transition(:closing)
827
+ end
789
828
  parser.on(:close) do
790
829
  reset
791
830
  end
@@ -793,15 +832,16 @@ module HTTPX
793
832
  consume unless @state == :closed
794
833
  end
795
834
  parser.on(:reset) do
796
- enqueue_pending_requests_from_parser(parser)
835
+ parser.reset_requests
797
836
 
798
837
  reset
799
838
 
800
839
  next unless @state == :closed
801
840
 
841
+ # @fiber-switch-guard
802
842
  # :reset event only fired in http/1.1, so this guarantees
803
843
  # that the connection will be closed here.
804
- idling unless @pending.empty?
844
+ call unless no_more_requests?
805
845
  end
806
846
  parser.on(:current_timeout) do
807
847
  @current_timeout = @timeout = parser.timeout
@@ -809,29 +849,33 @@ module HTTPX
809
849
  parser.on(:timeout) do |tout|
810
850
  @timeout = tout
811
851
  end
812
- parser.on(:error) do |request, error|
813
- case error
814
- when :http_1_1_required
815
- current_session = @current_session
816
- current_selector = @current_selector
817
- parser.close
818
-
819
- request.transition(:idle)
820
- current_session.__send__(:send_request, request, current_selector,
821
- @options.merge(ssl: { alpn_protocols: %w[http/1.1] })) do |other_conn|
822
- other_conn.merge(self)
823
- end
824
- next
825
- when OperationTimeoutError
826
- # request level timeouts should take precedence
827
- next unless request.active_timeouts.empty?
828
- end
852
+ parser.on(:keep_alive_timeout) do |tout|
853
+ @keep_alive_timeout = tout
854
+ end
855
+ parser.on(:error, &method(:on_parser_error))
856
+ end
829
857
 
830
- @inflight -= 1
831
- response = ErrorResponse.new(request, error)
832
- request.response = response
833
- request.emit_response(response)
858
+ def on_parser_error(request, error)
859
+ case error
860
+ when :http_1_1_required
861
+ current_session = @current_session
862
+ current_selector = @current_selector
863
+ parser.close
864
+
865
+ request.transition(:idle)
866
+ current_session.__send__(:send_request, request, current_selector,
867
+ @options.merge(ssl: { alpn_protocols: %w[http/1.1] })) do |other_conn|
868
+ other_conn.merge(self)
869
+ end
870
+ return
871
+ when OperationTimeoutError
872
+ # request level timeouts should take precedence
873
+ return unless request.active_timeouts.empty?
834
874
  end
875
+
876
+ response = ErrorResponse.new(request, error)
877
+ request.response = response
878
+ request.emit_response(response)
835
879
  end
836
880
 
837
881
  def transition(nextstate)
@@ -882,21 +926,10 @@ module HTTPX
882
926
  emit(:open)
883
927
  when :inactive
884
928
  return unless @state == :open
885
-
886
- # @type ivar @parser: HTTP1 | HTTP2
887
-
888
- # do not deactivate connection in use
889
- return if @inflight.positive? || @parser.waiting_for_ping?
890
929
  when :closing
891
930
  return unless connecting? || @state == :open
892
931
  when :closed
893
- return unless @state == :closing
894
- return unless @write_buffer.empty?
895
-
896
- purge_after_closed
897
-
898
- # @fiber-switch-guard
899
- return unless @state == :closing && (@io.nil? || @io.can_disconnect?)
932
+ return unless @state == :closing && @write_buffer.empty?
900
933
  when :already_open
901
934
  nextstate = :open
902
935
  # the first check for given io readiness must still use a timeout.
@@ -926,8 +959,10 @@ module HTTPX
926
959
  consume
927
960
  @write_buffer.clear
928
961
  when :closed
929
- # TODO: should this raise an error instead?
930
- return unless @pending.empty?
962
+ purge_after_closed
963
+
964
+ # @fiber-switch-guard
965
+ return unless no_more_requests?
931
966
 
932
967
  disconnect
933
968
  end
@@ -1092,81 +1127,42 @@ module HTTPX
1092
1127
  "coalesced?:#{coalesced?})"
1093
1128
  end
1094
1129
 
1095
- def no_more_requests_loop_check
1096
- log(level: 3) { "NO MORE REQUESTS..." }
1097
- @no_more_requests_counter += 1
1098
-
1099
- return if @no_more_requests_counter < 50
1100
-
1101
- per_request_state = @parser.pending.map do |req|
1102
- "[state:#{req.state},ping?:#{req.ping?}]"
1103
- end.join(",")
1104
-
1105
- parser_state =
1106
- case parser
1107
- when HTTP2
1108
- parser_pending_requests = parser.pending.map do |r|
1109
- num_retries = r.respond_to(:retries) ? r.retries : nil
1110
- req_conn = r.instance_variable_get(:@connection)
1111
- "id:#{r.object_id},num_retries:#{num_retries}," \
1112
- "is-same-conn:#{req_conn.equal?(self)},started?:#{r.started?}"
1113
- end.join(",")
1114
- +"parser-streams:#{parser.streams.size}," \
1115
- "parser-handshake-completed:#{parser.instance_variable_get(:@handshake_completed)}," \
1116
- "parser-wait-for-handshake:#{parser.instance_variable_get(:@wait_for_handshake)}," \
1117
- "parser-pending:[#{parser_pending_requests}],"
1118
- else
1119
- +""
1120
- end
1121
-
1122
- parser_state << "parser-requests:#{per_request_state}"
1123
-
1124
- raise Error, "connection corrupted, aborted after looping for a while, " \
1125
- "please report this https://gitlab.com/os85/httpx/-/work_items " \
1126
- "along with debug logs\n(" \
1127
- "state:#{@state},inflight:#{@inflight},pending:#{@pending.size}," \
1128
- "exhausted:#{@exhausted}," \
1129
- "previously-exhausted-with-error:#{@previously_exhausted_with_error}," \
1130
- "connected_for_secs:#{Utils.elapsed_time(@connected_at) if @connected_at}," \
1131
- "num-origins:#{@origins.size}," \
1132
- "last_response_received:#{Utils.elapsed_time(@response_received_at) if @response_received_at}," \
1133
- "#{parser_state}," \
1134
- "in-selector:#{!!@current_selector}," \
1135
- "in-session:#{!!@current_session}" \
1136
- ")"
1130
+ def clear_pending_requests
1131
+ @pending.clear
1137
1132
  end
1138
1133
 
1139
1134
  # true when there are no more pending nor inflight (in parser) requests
1140
1135
  def no_more_requests?
1141
- @pending.empty? && @inflight.zero?
1136
+ @pending.empty? && (
1137
+ connecting? ||
1138
+ @parser.nil? ||
1139
+ !@parser.respond_to?(:no_more_requests?) ||
1140
+ @parser.no_more_requests?
1141
+ )
1142
1142
  end
1143
1143
 
1144
+ alias_method :no_requests_to_handle?, :no_more_requests?
1144
1145
  # recover internal state and emit all relevant error responses when +error+ was raised.
1145
1146
  # this takes an optiona +request+ which may have already been handled and can be opted out
1146
1147
  # in the state recovery process.
1147
1148
  def handle_error(error, request = nil)
1148
1149
  if request
1149
- @inflight -= 1
1150
1150
  response = ErrorResponse.new(request, error)
1151
1151
  request.response = response
1152
1152
  request.emit_response(response)
1153
1153
  end
1154
1154
 
1155
- pending = @pending
1156
1155
  if (parser = @parser) && parser.respond_to?(:handle_error)
1157
1156
  # parser.handle_error may disconnect the connection
1158
- pending = @pending.dup
1159
- @pending = []
1160
-
1161
1157
  parser.handle_error(error, request)
1162
- end
1163
-
1164
- while (req = pending.shift)
1165
- next if request && req == request
1158
+ else
1159
+ while (req = @pending.shift)
1160
+ next if request && req == request
1166
1161
 
1167
- resp = ErrorResponse.new(req, error)
1168
- req.response = resp
1169
- req.emit_response(resp)
1162
+ resp = ErrorResponse.new(req, error)
1163
+ req.response = resp
1164
+ req.emit_response(resp)
1165
+ end
1170
1166
  end
1171
1167
  end
1172
1168
 
data/lib/httpx/io/tcp.rb CHANGED
@@ -17,7 +17,7 @@ module HTTPX
17
17
  @keep_open = false
18
18
  @addresses = []
19
19
  @ip_index = -1
20
- @ip = nil
20
+ @ip = @attempt_started_at = nil
21
21
  @hostname = origin.host
22
22
  @options = options
23
23
  @fallback_protocol = @options.fallback_protocol
@@ -80,6 +80,27 @@ module HTTPX
80
80
  @addresses.any?
81
81
  end
82
82
 
83
+ # time left until the attempt to connect to the current address should be abandoned.
84
+ def connect_attempt_time_left
85
+ return unless @state == :idle && @ip_index.positive? && (attempt_started_at = @attempt_started_at)
86
+
87
+ timeouts = @options.timeout
88
+ timeout = timeouts.connect_timeout || timeouts.request_timeout || timeouts.total_request_timeout
89
+
90
+ return unless timeout && timeout.finite?
91
+
92
+ timeout.fdiv(@addresses.size) - Utils.elapsed_time(attempt_started_at)
93
+ end
94
+
95
+ # abandons the connection attempt to the current address.
96
+ def next_address!(e)
97
+ @io&.close
98
+ @ip_index -= 1
99
+ raise e if @ip_index.negative?
100
+
101
+ log { "failed connecting to #{@ip} (#{e.message}), trying next..." }
102
+ end
103
+
83
104
  def to_io
84
105
  @io.to_io
85
106
  end
@@ -109,13 +130,8 @@ module HTTPX
109
130
  try_connect
110
131
  rescue Errno::EHOSTUNREACH,
111
132
  Errno::ENETUNREACH => e
112
- @ip_index -= 1
113
-
114
- raise e if @ip_index.negative?
115
-
116
- log { "failed connecting to #{@ip} (#{e.message}), evict from cache and trying next..." }
133
+ next_address!(e)
117
134
  @options.resolver_cache.evict(@hostname, @ip)
118
-
119
135
  @io = build_socket
120
136
  retry
121
137
  rescue Errno::ECONNREFUSED,
@@ -237,6 +253,7 @@ module HTTPX
237
253
 
238
254
  def build_socket
239
255
  @ip = @addresses[@ip_index]
256
+ @attempt_started_at = Utils.now
240
257
  Socket.new(@ip.family, :STREAM, 0)
241
258
  end
242
259
 
@@ -156,7 +156,6 @@ module HTTPX
156
156
 
157
157
  class << self
158
158
  def load_dependencies(*)
159
- require "set"
160
159
  require "digest/sha2"
161
160
  require "cgi/escape"
162
161
  end
@@ -106,7 +106,7 @@ module HTTPX
106
106
  def fetch_response(request, *)
107
107
  response = super
108
108
 
109
- return unless response
109
+ return unless response && response.is_a?(Response)
110
110
 
111
111
  if cacheable_request?(request) && cacheable_response?(request, response) && !response.cached?
112
112
  log { "caching response for #{request.uri}..." }
@@ -34,6 +34,7 @@ module HTTPX
34
34
  end
35
35
 
36
36
  def plugin(*args, &blk)
37
+ # ruby 2.7 requires explicit named splat
37
38
  super(*args).tap do |sess|
38
39
  CALLBACKS.each do |cb|
39
40
  next unless callbacks_for?(cb)