httpx 1.8.4 → 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 (97) 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 +75 -0
  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 +15 -2
  10. data/lib/httpx/altsvc.rb +29 -15
  11. data/lib/httpx/chainable.rb +4 -1
  12. data/lib/httpx/connection/http1.rb +34 -35
  13. data/lib/httpx/connection/http2.rb +146 -33
  14. data/lib/httpx/connection.rb +282 -162
  15. data/lib/httpx/headers.rb +2 -2
  16. data/lib/httpx/io/tcp.rb +42 -21
  17. data/lib/httpx/options.rb +86 -35
  18. data/lib/httpx/parser/http1.rb +3 -6
  19. data/lib/httpx/plugins/auth/digest.rb +1 -2
  20. data/lib/httpx/plugins/auth.rb +11 -2
  21. data/lib/httpx/plugins/aws_sdk_authentication.rb +7 -3
  22. data/lib/httpx/plugins/aws_sigv4.rb +1 -1
  23. data/lib/httpx/plugins/basic_auth.rb +2 -0
  24. data/lib/httpx/plugins/cache.rb +1 -1
  25. data/lib/httpx/plugins/callbacks.rb +8 -6
  26. data/lib/httpx/plugins/circuit_breaker.rb +1 -0
  27. data/lib/httpx/plugins/content_digest.rb +3 -5
  28. data/lib/httpx/plugins/digest_auth.rb +2 -0
  29. data/lib/httpx/plugins/expect.rb +10 -1
  30. data/lib/httpx/plugins/fiber_concurrency.rb +59 -16
  31. data/lib/httpx/plugins/follow_redirects.rb +1 -1
  32. data/lib/httpx/plugins/grpc.rb +21 -0
  33. data/lib/httpx/plugins/h2c.rb +12 -11
  34. data/lib/httpx/plugins/hedging.rb +284 -0
  35. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  36. data/lib/httpx/plugins/ntlm_v2_auth.rb +5 -3
  37. data/lib/httpx/plugins/oauth.rb +2 -2
  38. data/lib/httpx/plugins/proxy/http.rb +67 -26
  39. data/lib/httpx/plugins/proxy/socks5.rb +4 -4
  40. data/lib/httpx/plugins/proxy/ssh.rb +30 -11
  41. data/lib/httpx/plugins/proxy.rb +68 -54
  42. data/lib/httpx/plugins/push_promise.rb +4 -2
  43. data/lib/httpx/plugins/query.rb +1 -0
  44. data/lib/httpx/plugins/retries.rb +2 -2
  45. data/lib/httpx/plugins/server_sent_events.rb +1 -0
  46. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  47. data/lib/httpx/plugins/stream.rb +12 -10
  48. data/lib/httpx/plugins/stream_bidi.rb +4 -0
  49. data/lib/httpx/plugins/tracing.rb +99 -11
  50. data/lib/httpx/plugins/upgrade/h2.rb +3 -3
  51. data/lib/httpx/plugins/upgrade.rb +2 -0
  52. data/lib/httpx/plugins/webdav.rb +8 -0
  53. data/lib/httpx/request/body.rb +2 -1
  54. data/lib/httpx/request.rb +6 -6
  55. data/lib/httpx/resolver/https.rb +3 -2
  56. data/lib/httpx/resolver/native.rb +19 -9
  57. data/lib/httpx/resolver/resolver.rb +2 -2
  58. data/lib/httpx/resolver/system.rb +4 -0
  59. data/lib/httpx/resolver.rb +10 -6
  60. data/lib/httpx/response.rb +19 -7
  61. data/lib/httpx/selector.rb +111 -10
  62. data/lib/httpx/session.rb +36 -10
  63. data/lib/httpx/transcoder/json.rb +1 -1
  64. data/lib/httpx/version.rb +1 -1
  65. data/lib/httpx.rb +1 -0
  66. data/sig/altsvc.rbs +3 -3
  67. data/sig/callbacks.rbs +1 -0
  68. data/sig/chainable.rbs +3 -1
  69. data/sig/connection/http1.rbs +4 -2
  70. data/sig/connection/http2.rbs +23 -17
  71. data/sig/connection.rbs +29 -7
  72. data/sig/headers.rbs +1 -1
  73. data/sig/httpx.rbs +2 -0
  74. data/sig/io/tcp.rbs +5 -0
  75. data/sig/options.rbs +43 -12
  76. data/sig/parser/http1.rbs +1 -1
  77. data/sig/plugins/auth/digest.rbs +0 -1
  78. data/sig/plugins/auth.rbs +12 -5
  79. data/sig/plugins/expect.rbs +6 -0
  80. data/sig/plugins/fiber_concurrency.rbs +10 -1
  81. data/sig/plugins/hedging.rbs +44 -0
  82. data/sig/plugins/proxy/http.rbs +9 -2
  83. data/sig/plugins/proxy.rbs +3 -2
  84. data/sig/plugins/retries.rbs +2 -2
  85. data/sig/plugins/stream_bidi.rbs +0 -2
  86. data/sig/plugins/tracing.rbs +22 -5
  87. data/sig/request/body.rbs +1 -1
  88. data/sig/resolver/entry.rbs +2 -2
  89. data/sig/resolver/https.rbs +2 -0
  90. data/sig/resolver/native.rbs +3 -0
  91. data/sig/resolver/resolver.rbs +1 -1
  92. data/sig/resolver/system.rbs +2 -0
  93. data/sig/resolver.rbs +1 -1
  94. data/sig/response.rbs +1 -2
  95. data/sig/selector.rbs +17 -3
  96. data/sig/session.rbs +8 -6
  97. metadata +7 -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
 
@@ -52,9 +50,10 @@ module HTTPX
52
50
  @io = @ssl_session = @timeout = @connected_at = @response_received_at = nil
53
51
 
54
52
  @exhausted = @cloned = @main_sibling =
55
- # variable used to gate against a potential endless loop where the peer continuously closes the connection with
56
- # GOAWAY frames without ever processing a request.
57
- @previously_exhausted_with_error = false
53
+ @reset_timeout_on_call =
54
+ # variable used to gate against a potential endless loop where the peer continuously closes the connection with
55
+ # GOAWAY frames without ever processing a request.
56
+ @previously_exhausted_with_error = false
58
57
 
59
58
  @options = Options.new(options)
60
59
  @type = initialize_type(uri, @options)
@@ -64,8 +63,7 @@ module HTTPX
64
63
  @read_buffer = Buffer.new(@options.buffer_size)
65
64
  @write_buffer = Buffer.new(@options.buffer_size)
66
65
  @pending = []
67
- @inflight = 0
68
- @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
66
+ @keep_alive_timeout = @options.timeout.keep_alive_timeout
69
67
  @no_more_requests_counter = 0
70
68
 
71
69
  if @options.io
@@ -77,7 +75,14 @@ module HTTPX
77
75
  else
78
76
  transition(:idle)
79
77
  end
80
- self.addresses = @options.addresses if @options.addresses
78
+
79
+ case (addresses = @options.addresses)
80
+ when Array
81
+ self.addresses = addresses
82
+ when Hash
83
+ # TODO: && @origin.host not an IP
84
+ self.addresses = addresses[@origin.host]
85
+ end
81
86
  end
82
87
 
83
88
  # dupped initialization
@@ -87,7 +92,6 @@ module HTTPX
87
92
  @origins = orig.origins.dup
88
93
  @read_buffer = orig.read_buffer.dup
89
94
  @write_buffer = orig.write_buffer.dup
90
- @inflight = 0
91
95
  @pending = []
92
96
  transition(:idle)
93
97
 
@@ -126,7 +130,7 @@ module HTTPX
126
130
  end
127
131
 
128
132
  def match?(uri, options)
129
- return false if !used? && (@state == :closing || @state == :closed)
133
+ return false if @state == :closing || (!used? && @state == :closed)
130
134
 
131
135
  @origins.include?(uri.origin) &&
132
136
  # if there is more than one origin to match, it means that this connection
@@ -140,6 +144,9 @@ module HTTPX
140
144
  def mergeable?(connection)
141
145
  return false if @state == :closing || @state == :closed || !@io
142
146
 
147
+ # do not coalesce with HEv2 sibling
148
+ return false if @sibling.equal?(connection)
149
+
143
150
  return false unless connection.addresses
144
151
 
145
152
  (
@@ -187,19 +194,16 @@ module HTTPX
187
194
  end
188
195
  end
189
196
 
197
+ # returns the number of pending requests both in the connection as well as parser
198
+ # pending queues
199
+ def pending_count
200
+ @pending.size
201
+ end
202
+
190
203
  def purge_pending(&block)
191
- if @parser
192
- pending = @parser.pending
193
- @inflight -= pending.size
194
- pending.reject! do |req|
195
- block.call(req)
196
- true
197
- end
198
- end
199
- @pending.reject! do |req|
200
- block.call(req)
201
- true
202
- end
204
+ pending = @pending.dup
205
+ @pending.clear
206
+ pending.each(&block)
203
207
  end
204
208
 
205
209
  def io_connected?
@@ -212,6 +216,11 @@ module HTTPX
212
216
  @state == :idle
213
217
  end
214
218
 
219
+ # whether the connection can be removed from the selector.
220
+ def closed_for_select?
221
+ @state == :closed
222
+ end
223
+
215
224
  def inflight?
216
225
  @parser && (
217
226
  # parser may be dealing with other requests (possibly started from a different fiber)
@@ -224,14 +233,12 @@ module HTTPX
224
233
  def interests
225
234
  # connecting
226
235
  if connecting?
227
- connect
236
+ try_connect
228
237
 
229
238
  return @io.interests if connecting?
230
239
  end
231
240
 
232
- return @parser.interests if @parser
233
-
234
- nil
241
+ @parser&.interests
235
242
  rescue Error => e
236
243
  on_error(e)
237
244
  nil
@@ -242,18 +249,23 @@ module HTTPX
242
249
  end
243
250
 
244
251
  def call
252
+ if @reset_timeout_on_call
253
+ @timeout = nil
254
+ @reset_timeout_on_call = false
255
+ end
256
+
245
257
  case @state
246
258
  when :idle
247
259
  return if no_more_requests?
248
260
 
249
- connect
261
+ try_connect
250
262
 
251
263
  # when opening the tcp or ssl socket fails
252
264
  return if @state == :closed
253
265
 
254
266
  consume
255
267
  when :closed
256
- return if no_more_requests?
268
+ return if no_requests_to_handle?
257
269
 
258
270
  # there are pending requests to send, restart the state machine.
259
271
  idling
@@ -264,8 +276,18 @@ module HTTPX
264
276
 
265
277
  call
266
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
+
267
282
  consume
268
- 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
269
291
 
270
292
  # @fiber-switch-guard
271
293
  # fiber may have switch while closing @io.
@@ -300,7 +322,7 @@ module HTTPX
300
322
  def close
301
323
  transition(:active) if @state == :inactive
302
324
 
303
- @parser.close if @parser
325
+ @parser&.close
304
326
  end
305
327
 
306
328
  def terminate
@@ -310,6 +332,7 @@ module HTTPX
310
332
 
311
333
  # @fiber-switch-guard
312
334
  if @io.can_disconnect? && @pending.empty?
335
+ @state = :closed
313
336
  disconnect
314
337
  return
315
338
  end
@@ -329,11 +352,10 @@ module HTTPX
329
352
  if delete_pending
330
353
  @pending.clear
331
354
  elsif (parser = @parser)
332
- enqueue_pending_requests_from_parser(parser)
355
+ parser.reset_requests if parser.respond_to?(:reset_requests)
356
+ return unless @pending.empty?
333
357
  end
334
358
 
335
- return unless @pending.empty?
336
-
337
359
  disconnect
338
360
  emit(:force_closed, delete_pending)
339
361
  end
@@ -347,20 +369,19 @@ module HTTPX
347
369
  end
348
370
 
349
371
  def reset
350
- return if @state == :closing || @state == :closed
372
+ return if @state == :closed
351
373
 
352
374
  # do not reset a connection which may have restarted back to :idle, such when the parser resets
353
375
  # (example: HTTP/1 parser disabling pipelining)
354
376
  return if @state == :idle && @pending.any?
355
377
 
356
- if @ping_timer
357
- @ping_timer.cancel
358
- @ping_timer = nil
359
- end
378
+ reset_ping_timer
360
379
 
361
380
  parser = @parser
362
381
 
363
382
  if parser && parser.respond_to?(:max_concurrent_requests)
383
+ # @type var parser: HTTP1
384
+
364
385
  # if connection being reset has at some downgraded the number of concurrent
365
386
  # requests, such as in the case where an attempt to use HTTP/1 pipelining failed,
366
387
  # keep that information around.
@@ -374,6 +395,8 @@ module HTTPX
374
395
  def send(request)
375
396
  return @coalesced_connection.send(request) if @coalesced_connection
376
397
 
398
+ request.connection = self
399
+
377
400
  if @parser && !@write_buffer.full?
378
401
  if @response_received_at && @keep_alive_timeout &&
379
402
  Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
@@ -390,6 +413,8 @@ module HTTPX
390
413
 
391
414
  send_request_to_parser(request)
392
415
  else
416
+ set_connection_timeouts(request) if connecting?
417
+
393
418
  @pending << request
394
419
  end
395
420
  end
@@ -397,11 +422,14 @@ module HTTPX
397
422
  def timeout
398
423
  return if @state == :closed || @state == :inactive
399
424
 
400
- return @timeout if @timeout
425
+ timeout = @timeout || @options.timeout.operation_timeout
401
426
 
402
- return @options.timeout[:connect_timeout] if @state == :idle
427
+ return timeout unless connecting? && (attempt_timeout = connect_attempt_time_left)
403
428
 
404
- @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
405
433
  end
406
434
 
407
435
  def idling
@@ -413,7 +441,7 @@ module HTTPX
413
441
  transition(:idle)
414
442
  return unless @parser
415
443
 
416
- enqueue_pending_requests_from_parser(parser)
444
+ @parser.reset_requests if @parser.respond_to?(:reset_requests)
417
445
  @parser = nil
418
446
  end
419
447
 
@@ -422,6 +450,9 @@ module HTTPX
422
450
  end
423
451
 
424
452
  def deactivate
453
+ # safety net so that connection state does not corrupt.
454
+ clear_pending_requests
455
+
425
456
  transition(:inactive)
426
457
  end
427
458
 
@@ -463,6 +494,9 @@ module HTTPX
463
494
 
464
495
  @current_session = @current_selector = nil
465
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.
466
500
  current_session.deselect_connection(self, current_selector, @cloned)
467
501
  end
468
502
 
@@ -486,16 +520,37 @@ module HTTPX
486
520
 
487
521
  def on_error(error, request = nil)
488
522
  if error.is_a?(OperationTimeoutError)
489
-
490
523
  # inactive connections do not contribute to the select loop, therefore
491
524
  # they should not fail due to such errors.
492
525
  return if @state == :inactive
493
526
 
494
- if @timeout
495
- @timeout -= error.timeout
496
- return unless @timeout <= 0
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
533
+ # if @timeout isn't set, it's not a completion-based timeout, i.e.
534
+ # operation timeout; in such a case, we have to reset it once it piggybacks.
535
+ @reset_timeout_on_call ||= @timeout.nil?
536
+
537
+ @timeout = current_timeout - error.timeout
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
497
544
 
498
545
  @timeout = nil
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
499
554
  end
500
555
 
501
556
  error = error.to_connection_error if connecting?
@@ -516,6 +571,26 @@ module HTTPX
516
571
 
517
572
  private
518
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
+
589
+ def try_connect
590
+ emit(:connecting)
591
+ connect
592
+ end
593
+
519
594
  def connect
520
595
  transition(:open)
521
596
  end
@@ -539,7 +614,6 @@ module HTTPX
539
614
  # * the number of inflight requests
540
615
  # * whether the write buffer has bytes (i.e. for close handshake)
541
616
  if no_more_requests? && @write_buffer.empty?
542
- no_more_requests_loop_check if @parser && @parser.pending.any?
543
617
 
544
618
  # terminate if an altsvc connection has been established
545
619
  terminate if @altsvc_connection
@@ -582,21 +656,19 @@ module HTTPX
582
656
  parser << @read_buffer.to_s
583
657
 
584
658
  # continue reading if possible.
585
- break if interests == :w && !epiped
586
-
587
- # exit the read loop if connection is preparing to be closed
588
- break if @state == :closing || @state == :closed
659
+ break if (interests == :w && !epiped) ||
660
+ # exit the read loop if connection is closed
661
+ @state == :closed
589
662
 
590
663
  # exit #consume altogether if all outstanding requests have been dealt with
591
664
  if no_more_requests? && @write_buffer.empty? # rubocop:disable Style/Next
592
- no_more_requests_loop_check if @parser && @parser.pending.any?
593
665
 
594
666
  # terminate if an altsvc connection has been established
595
667
  terminate if @altsvc_connection
596
668
 
597
669
  return
598
670
  end
599
- end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
671
+ end unless ((ints = interests).nil? || ints == :w) && !epiped
600
672
 
601
673
  #
602
674
  # tight write loop.
@@ -607,7 +679,7 @@ module HTTPX
607
679
  # buffer has been drained, mark and exit the write loop.
608
680
  if @write_buffer.empty?
609
681
  # we only mark as drained on the first loop
610
- write_drained = write_drained.nil? && @inflight.positive?
682
+ write_drained = write_drained.nil? && !@parser&.no_more_requests?
611
683
 
612
684
  break
613
685
  end
@@ -642,11 +714,12 @@ module HTTPX
642
714
  end
643
715
 
644
716
  # exit write loop if marked to consume from peer, or is closing.
645
- break if interests == :r || @state == :closing || @state == :closed
717
+ break if (ints = interests) == :r || @state == :closed
646
718
 
647
719
  write_drained = false
648
720
  end unless (ints = interests) == :r
649
721
 
722
+ # fill the write buffer with more requests
650
723
  send_pending if @state == :open
651
724
 
652
725
  # return if socket is drained
@@ -663,8 +736,9 @@ module HTTPX
663
736
  end
664
737
 
665
738
  def send_pending
666
- while !@write_buffer.full? && (request = @pending.shift)
667
- 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)
668
742
  end
669
743
  end
670
744
 
@@ -672,44 +746,29 @@ module HTTPX
672
746
  @parser ||= build_parser
673
747
  end
674
748
 
675
- def send_request_to_parser(request)
676
- @inflight += 1
677
- request.peer_address = @io.ip && @io.ip.address
678
- set_request_timeouts(request)
679
-
680
- parser.send(request)
749
+ def send_request_to_parser(request, head = false)
750
+ request.peer_address = @io&.ip&.address
681
751
 
682
- return unless @state == :inactive
683
-
684
- transition(:active)
685
- # mark request as ping, as this inactive connection may have been
686
- # closed by the server, and we don't want that to influence retry
687
- # bookkeeping.
688
- request.ping!
689
- end
690
-
691
- def enqueue_pending_requests_from_parser(parser)
692
- parser.reset_requests # move sequential requests back to pending queue.
693
- parser_pending_requests = parser.pending
694
-
695
- return if parser_pending_requests.empty?
696
-
697
- # the connection will be reused, so parser requests must come
698
- # back to the pending list before the parser is reset.
699
- @inflight -= parser_pending_requests.size
700
- @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
701
759
 
702
- parser.pending.clear
760
+ parser.send(request, head)
703
761
  end
704
762
 
705
763
  def build_parser(protocol = @io.protocol)
706
- parser = parser_type(protocol).new(@write_buffer, @options)
764
+ parser = parser_type(protocol).new(@write_buffer, @pending, @options)
707
765
  set_parser_callbacks(parser)
708
766
  parser.max_concurrent_requests = @max_concurrent_requests if @max_concurrent_requests && parser.respond_to?(:max_concurrent_requests=)
709
767
  parser
710
768
  end
711
769
 
712
770
  def set_parser_callbacks(parser)
771
+ parser.on(:request_accepted, &method(:set_request_timeouts))
713
772
  parser.on(:response) do |request, response|
714
773
  AltSvc.emit(request, response) do |alt_origin, origin, alt_params|
715
774
  build_altsvc_connection(alt_origin, origin, alt_params)
@@ -717,7 +776,6 @@ module HTTPX
717
776
  @previously_exhausted_with_error = false
718
777
  @response_received_at = Utils.now
719
778
  @no_more_requests_counter = 0
720
- @inflight -= 1
721
779
  response.finish!
722
780
  request.emit_response(response)
723
781
  end
@@ -731,7 +789,7 @@ module HTTPX
731
789
  request.emit(:promise, parser, stream)
732
790
  end
733
791
  parser.on(:exhausted) do |error|
734
- enqueue_pending_requests_from_parser(parser)
792
+ parser.reset_requests
735
793
 
736
794
  if error
737
795
  if @previously_exhausted_with_error
@@ -739,6 +797,9 @@ module HTTPX
739
797
  on_error(error)
740
798
  next
741
799
  else
800
+ # in case the error came while waiting for a PING ack.
801
+ reset_ping_timer
802
+
742
803
  @previously_exhausted_with_error = true
743
804
  end
744
805
  end
@@ -757,6 +818,13 @@ module HTTPX
757
818
  parser.on(:origin) do |origin|
758
819
  @origins |= [origin]
759
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
760
828
  parser.on(:close) do
761
829
  reset
762
830
  end
@@ -764,15 +832,16 @@ module HTTPX
764
832
  consume unless @state == :closed
765
833
  end
766
834
  parser.on(:reset) do
767
- enqueue_pending_requests_from_parser(parser)
835
+ parser.reset_requests
768
836
 
769
837
  reset
770
838
 
771
839
  next unless @state == :closed
772
840
 
841
+ # @fiber-switch-guard
773
842
  # :reset event only fired in http/1.1, so this guarantees
774
843
  # that the connection will be closed here.
775
- idling unless @pending.empty?
844
+ call unless no_more_requests?
776
845
  end
777
846
  parser.on(:current_timeout) do
778
847
  @current_timeout = @timeout = parser.timeout
@@ -780,29 +849,33 @@ module HTTPX
780
849
  parser.on(:timeout) do |tout|
781
850
  @timeout = tout
782
851
  end
783
- parser.on(:error) do |request, error|
784
- case error
785
- when :http_1_1_required
786
- current_session = @current_session
787
- current_selector = @current_selector
788
- parser.close
789
-
790
- other_connection = current_session.find_connection(@origin, current_selector,
791
- @options.merge(ssl: { alpn_protocols: %w[http/1.1] }))
792
- other_connection.merge(self)
793
- request.transition(:idle)
794
- other_connection.send(request)
795
- next
796
- when OperationTimeoutError
797
- # request level timeouts should take precedence
798
- next unless request.active_timeouts.empty?
799
- 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
800
857
 
801
- @inflight -= 1
802
- response = ErrorResponse.new(request, error)
803
- request.response = response
804
- 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?
805
874
  end
875
+
876
+ response = ErrorResponse.new(request, error)
877
+ request.response = response
878
+ request.emit_response(response)
806
879
  end
807
880
 
808
881
  def transition(nextstate)
@@ -834,7 +907,7 @@ module HTTPX
834
907
  def handle_transition(nextstate)
835
908
  case nextstate
836
909
  when :idle
837
- @timeout = @current_timeout = @options.timeout[:connect_timeout]
910
+ @timeout = @current_timeout = @options.timeout.connect_timeout
838
911
 
839
912
  @connected_at = @response_received_at = nil
840
913
  when :open
@@ -853,32 +926,23 @@ module HTTPX
853
926
  emit(:open)
854
927
  when :inactive
855
928
  return unless @state == :open
856
-
857
- # @type ivar @parser: HTTP1 | HTTP2
858
-
859
- # do not deactivate connection in use
860
- return if @inflight.positive? || @parser.waiting_for_ping?
861
929
  when :closing
862
930
  return unless connecting? || @state == :open
863
931
  when :closed
864
- return unless @state == :closing
865
- return unless @write_buffer.empty?
866
-
867
- purge_after_closed
868
-
869
- # @fiber-switch-guard
870
- return unless @state == :closing && (@io.nil? || @io.can_disconnect?)
932
+ return unless @state == :closing && @write_buffer.empty?
871
933
  when :already_open
872
934
  nextstate = :open
873
935
  # the first check for given io readiness must still use a timeout.
874
936
  # connect is the reasonable choice in such a case.
875
- @timeout = @options.timeout[:connect_timeout]
937
+ @timeout = @options.timeout.connect_timeout
876
938
  send_pending
877
939
  when :active
878
940
  return unless @state == :inactive
879
941
 
880
942
  nextstate = :open
881
943
 
944
+ @timeout = @current_timeout = parser.timeout
945
+
882
946
  # activate
883
947
  @current_session.select_connection(self, @current_selector)
884
948
  end
@@ -895,8 +959,10 @@ module HTTPX
895
959
  consume
896
960
  @write_buffer.clear
897
961
  when :closed
898
- # TODO: should this raise an error instead?
899
- return unless @pending.empty?
962
+ purge_after_closed
963
+
964
+ # @fiber-switch-guard
965
+ return unless no_more_requests?
900
966
 
901
967
  disconnect
902
968
  end
@@ -916,6 +982,13 @@ module HTTPX
916
982
  end
917
983
  end
918
984
 
985
+ def reset_ping_timer
986
+ return unless @ping_timer
987
+
988
+ @ping_timer.cancel
989
+ @ping_timer = nil
990
+ end
991
+
919
992
  def close_sibling
920
993
  sibling = @sibling
921
994
 
@@ -963,8 +1036,6 @@ module HTTPX
963
1036
 
964
1037
  # returns an HTTPX::Connection for the negotiated Alternative Service (or none).
965
1038
  def build_altsvc_connection(alt_origin, origin, alt_params)
966
- return if @altsvc_connection
967
-
968
1039
  # do not allow security downgrades on altsvc negotiation
969
1040
  return if @origin.scheme == "https" && alt_origin.scheme != "https"
970
1041
 
@@ -973,6 +1044,8 @@ module HTTPX
973
1044
  # altsvc already exists, somehow it wasn't advertised, probably noop
974
1045
  return unless altsvc
975
1046
 
1047
+ return if @altsvc_connection
1048
+
976
1049
  alt_options = @options.merge(ssl: @options.ssl.merge(hostname: URI(origin).host))
977
1050
 
978
1051
  connection = @current_session.find_connection(alt_origin, @current_selector, alt_options)
@@ -1023,7 +1096,7 @@ module HTTPX
1023
1096
 
1024
1097
  return unless parser.waiting_for_ping?
1025
1098
 
1026
- ping_timeout = @options.timeout[:ping_timeout]
1099
+ ping_timeout = @options.timeout.ping_timeout
1027
1100
 
1028
1101
  @ping_timer = @current_selector.after(ping_timeout) do
1029
1102
  log(level: 3) { "ping timeout expired..." }
@@ -1035,66 +1108,76 @@ module HTTPX
1035
1108
  end
1036
1109
 
1037
1110
  def pong
1038
- @ping_timer.cancel
1039
- @ping_timer = nil
1111
+ reset_ping_timer
1040
1112
  @response_received_at = Utils.now
1041
1113
  @no_more_requests_counter = 0
1042
1114
  send_pending
1043
1115
  end
1044
1116
 
1045
- def no_more_requests_loop_check
1046
- log(level: 3) { "NO MORE REQUESTS..." }
1047
- @no_more_requests_counter += 1
1117
+ def no_more_selector_check
1118
+ return if @current_selector
1048
1119
 
1049
- return if @no_more_requests_counter < 50
1120
+ raise Error, "request has been resend to an out-of-session connection, and this " \
1121
+ "should never happen!!! Please report this error! " \
1122
+ "(state:#{@state}, " \
1123
+ "parser?:#{!!@parser}, " \
1124
+ "bytes in write buffer?:#{!@write_buffer.empty?}, " \
1125
+ "cloned?:#{@cloned}, " \
1126
+ "sibling?:#{!!@sibling}, " \
1127
+ "coalesced?:#{coalesced?})"
1128
+ end
1050
1129
 
1051
- raise Error, "connection corrupted, aborted after looping for a while, " \
1052
- "please report this https://gitlab.com/os85/httpx/-/work_items " \
1053
- "along with debug logs"
1130
+ def clear_pending_requests
1131
+ @pending.clear
1054
1132
  end
1055
1133
 
1056
1134
  # true when there are no more pending nor inflight (in parser) requests
1057
1135
  def no_more_requests?
1058
- @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
+ )
1059
1142
  end
1060
1143
 
1144
+ alias_method :no_requests_to_handle?, :no_more_requests?
1061
1145
  # recover internal state and emit all relevant error responses when +error+ was raised.
1062
1146
  # this takes an optiona +request+ which may have already been handled and can be opted out
1063
1147
  # in the state recovery process.
1064
1148
  def handle_error(error, request = nil)
1065
1149
  if request
1066
- @inflight -= 1
1067
1150
  response = ErrorResponse.new(request, error)
1068
1151
  request.response = response
1069
1152
  request.emit_response(response)
1070
1153
  end
1071
1154
 
1072
- pending = @pending
1073
1155
  if (parser = @parser) && parser.respond_to?(:handle_error)
1074
1156
  # parser.handle_error may disconnect the connection
1075
- pending = @pending.dup
1076
- @pending = []
1077
-
1078
1157
  parser.handle_error(error, request)
1079
- end
1080
-
1081
- while (req = pending.shift)
1082
- next if request && req == request
1158
+ else
1159
+ while (req = @pending.shift)
1160
+ next if request && req == request
1083
1161
 
1084
- resp = ErrorResponse.new(req, error)
1085
- req.response = resp
1086
- req.emit_response(resp)
1162
+ resp = ErrorResponse.new(req, error)
1163
+ req.response = resp
1164
+ req.emit_response(resp)
1165
+ end
1087
1166
  end
1088
1167
  end
1089
1168
 
1090
1169
  def set_request_timeouts(request)
1091
- request.connection = self
1092
1170
  set_request_write_timeout(request)
1093
1171
  set_request_read_timeout(request)
1094
1172
  set_request_request_timeout(request)
1095
1173
  set_request_total_request_timeout(request)
1096
1174
  end
1097
1175
 
1176
+ def set_connection_timeouts(request)
1177
+ set_connecting_request_timeout(request)
1178
+ set_connecting_total_request_timeout(request)
1179
+ end
1180
+
1098
1181
  def set_request_read_timeout(request)
1099
1182
  read_timeout = request.read_timeout
1100
1183
 
@@ -1158,19 +1241,52 @@ module HTTPX
1158
1241
  end
1159
1242
 
1160
1243
  def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
1244
+ return if request.active_timeouts.any? { |timer| timer.label == label }
1245
+
1161
1246
  request.set_timeout_callback(start_event) do
1162
- unless (selector = @current_selector)
1163
- raise Error, "request has been resend to an out-of-session connection, and this " \
1164
- "should never happen!!! Please report this error! " \
1165
- "(state:#{@state}, " \
1166
- "parser?:#{!!@parser}, " \
1167
- "bytes in write buffer?:#{!@write_buffer.empty?}, " \
1168
- "cloned?:#{@cloned}, " \
1169
- "sibling?:#{!!@sibling}, " \
1170
- "coalesced?:#{coalesced?})"
1247
+ no_more_selector_check
1248
+
1249
+ timer = @current_selector.after(timeout, callback)
1250
+ timer.label = label
1251
+ request.active_timeouts << timer
1252
+
1253
+ Array(finish_events).each do |event|
1254
+ # clean up request timeouts if the connection errors out
1255
+ request.set_timeout_callback(event) do
1256
+ timer.cancel
1257
+ request.active_timeouts.delete(timer)
1258
+ end
1171
1259
  end
1260
+ end
1261
+ end
1262
+
1263
+ def set_connecting_request_timeout(request)
1264
+ request_timeout = request.request_timeout
1265
+
1266
+ return if request_timeout.nil? || request_timeout.infinite?
1172
1267
 
1173
- timer = selector.after(timeout, callback)
1268
+ set_connection_timeout(:request_timeout, request, request_timeout, :connecting, :complete) do
1269
+ read_timeout_callback(request, request_timeout, RequestTimeoutError)
1270
+ end
1271
+ end
1272
+
1273
+ def set_connecting_total_request_timeout(request)
1274
+ return if request.started?
1275
+
1276
+ total_request_timeout = request.total_request_timeout
1277
+
1278
+ return if total_request_timeout.nil? || total_request_timeout.infinite?
1279
+
1280
+ set_connection_timeout(:total_request_timeout, request, total_request_timeout, :connecting, :complete) do
1281
+ read_timeout_callback(request, total_request_timeout, TotalRequestTimeoutError)
1282
+ end
1283
+ end
1284
+
1285
+ def set_connection_timeout(label, request, timeout, start_event, finish_events, &callback)
1286
+ cb = once(start_event) do
1287
+ no_more_selector_check
1288
+
1289
+ timer = @current_selector.after(timeout, callback)
1174
1290
  timer.label = label
1175
1291
  request.active_timeouts << timer
1176
1292
 
@@ -1182,6 +1298,10 @@ module HTTPX
1182
1298
  end
1183
1299
  end
1184
1300
  end
1301
+
1302
+ request.once(:idle) do
1303
+ callbacks(start_event).delete(cb)
1304
+ end
1185
1305
  end
1186
1306
 
1187
1307
  def parser_type(protocol)