httpx 1.7.2 → 1.7.6

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/doc/release_notes/1_7_3.md +29 -0
  4. data/doc/release_notes/1_7_4.md +42 -0
  5. data/doc/release_notes/1_7_5.md +10 -0
  6. data/doc/release_notes/1_7_6.md +24 -0
  7. data/lib/httpx/adapters/datadog.rb +37 -64
  8. data/lib/httpx/adapters/webmock.rb +3 -4
  9. data/lib/httpx/altsvc.rb +4 -2
  10. data/lib/httpx/connection/http1.rb +26 -18
  11. data/lib/httpx/connection/http2.rb +53 -33
  12. data/lib/httpx/connection.rb +152 -63
  13. data/lib/httpx/io/ssl.rb +20 -8
  14. data/lib/httpx/io/tcp.rb +18 -12
  15. data/lib/httpx/io/unix.rb +13 -9
  16. data/lib/httpx/options.rb +23 -7
  17. data/lib/httpx/parser/http1.rb +14 -4
  18. data/lib/httpx/plugins/auth/digest.rb +2 -1
  19. data/lib/httpx/plugins/auth.rb +23 -9
  20. data/lib/httpx/plugins/brotli.rb +33 -5
  21. data/lib/httpx/plugins/cookies/cookie.rb +34 -11
  22. data/lib/httpx/plugins/cookies/jar.rb +93 -18
  23. data/lib/httpx/plugins/cookies.rb +7 -3
  24. data/lib/httpx/plugins/expect.rb +33 -3
  25. data/lib/httpx/plugins/fiber_concurrency.rb +2 -4
  26. data/lib/httpx/plugins/follow_redirects.rb +7 -1
  27. data/lib/httpx/plugins/h2c.rb +1 -1
  28. data/lib/httpx/plugins/proxy/http.rb +15 -8
  29. data/lib/httpx/plugins/proxy.rb +10 -2
  30. data/lib/httpx/plugins/rate_limiter.rb +19 -19
  31. data/lib/httpx/plugins/retries.rb +17 -9
  32. data/lib/httpx/plugins/ssrf_filter.rb +1 -0
  33. data/lib/httpx/plugins/stream_bidi.rb +6 -0
  34. data/lib/httpx/plugins/tracing.rb +137 -0
  35. data/lib/httpx/pool.rb +7 -9
  36. data/lib/httpx/request.rb +15 -3
  37. data/lib/httpx/resolver/multi.rb +1 -8
  38. data/lib/httpx/resolver/native.rb +2 -2
  39. data/lib/httpx/resolver/resolver.rb +21 -2
  40. data/lib/httpx/resolver/system.rb +3 -1
  41. data/lib/httpx/response.rb +5 -1
  42. data/lib/httpx/selector.rb +19 -16
  43. data/lib/httpx/session.rb +34 -44
  44. data/lib/httpx/timers.rb +4 -0
  45. data/lib/httpx/version.rb +1 -1
  46. data/sig/altsvc.rbs +2 -0
  47. data/sig/chainable.rbs +2 -1
  48. data/sig/connection/http1.rbs +3 -1
  49. data/sig/connection/http2.rbs +11 -4
  50. data/sig/connection.rbs +16 -2
  51. data/sig/io/ssl.rbs +1 -0
  52. data/sig/io/tcp.rbs +2 -2
  53. data/sig/options.rbs +8 -3
  54. data/sig/parser/http1.rbs +1 -1
  55. data/sig/plugins/auth.rbs +5 -2
  56. data/sig/plugins/brotli.rbs +11 -6
  57. data/sig/plugins/cookies/cookie.rbs +3 -2
  58. data/sig/plugins/cookies/jar.rbs +11 -0
  59. data/sig/plugins/cookies.rbs +2 -0
  60. data/sig/plugins/expect.rbs +21 -2
  61. data/sig/plugins/fiber_concurrency.rbs +2 -2
  62. data/sig/plugins/proxy/socks4.rbs +4 -0
  63. data/sig/plugins/rate_limiter.rbs +2 -2
  64. data/sig/plugins/response_cache.rbs +3 -3
  65. data/sig/plugins/retries.rbs +17 -13
  66. data/sig/plugins/tracing.rbs +41 -0
  67. data/sig/pool.rbs +1 -1
  68. data/sig/request.rbs +4 -0
  69. data/sig/resolver/native.rbs +2 -0
  70. data/sig/resolver/resolver.rbs +4 -2
  71. data/sig/resolver/system.rbs +0 -2
  72. data/sig/response/body.rbs +1 -1
  73. data/sig/selector.rbs +7 -2
  74. data/sig/session.rbs +2 -0
  75. data/sig/timers.rbs +2 -0
  76. data/sig/transcoder/gzip.rbs +1 -1
  77. data/sig/transcoder.rbs +0 -2
  78. metadata +13 -3
@@ -45,10 +45,10 @@ module HTTPX
45
45
  protected :ssl_session, :sibling
46
46
 
47
47
  def initialize(uri, options)
48
- @current_session = @current_selector =
49
- @parser = @sibling = @coalesced_connection = @altsvc_connection =
50
- @family = @io = @ssl_session = @timeout =
51
- @connected_at = @response_received_at = nil
48
+ @current_session = @current_selector = @max_concurrent_requests =
49
+ @parser = @sibling = @coalesced_connection = @altsvc_connection =
50
+ @family = @io = @ssl_session = @timeout =
51
+ @connected_at = @response_received_at = nil
52
52
 
53
53
  @exhausted = @cloned = @main_sibling = false
54
54
 
@@ -62,6 +62,7 @@ module HTTPX
62
62
  @pending = []
63
63
  @inflight = 0
64
64
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
65
+ @no_more_requests_counter = 0
65
66
 
66
67
  if @options.io
67
68
  # if there's an already open IO, get its
@@ -106,7 +107,7 @@ module HTTPX
106
107
  # origin came from an ORIGIN frame, we're going to verify the hostname with the
107
108
  # SSL certificate
108
109
  (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host))) &&
109
- @options == options
110
+ @options.connection_options_match?(options)
110
111
  end
111
112
 
112
113
  def mergeable?(connection)
@@ -117,7 +118,7 @@ module HTTPX
117
118
  (
118
119
  (open? && @origin == connection.origin) ||
119
120
  !(@io.addresses & (connection.addresses || [])).empty?
120
- ) && @options == connection.options
121
+ ) && @options.connection_options_match?(connection.options)
121
122
  end
122
123
 
123
124
  # coalesces +self+ into +connection+.
@@ -154,6 +155,7 @@ module HTTPX
154
155
  end if @io
155
156
  end
156
157
  connection.purge_pending do |req|
158
+ req.transition(:idle)
157
159
  send(req)
158
160
  end
159
161
  end
@@ -161,8 +163,9 @@ module HTTPX
161
163
  def purge_pending(&block)
162
164
  pendings = []
163
165
  if @parser
164
- @inflight -= @parser.pending.size
165
- pendings << @parser.pending
166
+ pending = @parser.pending
167
+ @inflight -= pending.size
168
+ pendings << pending
166
169
  end
167
170
  pendings << @pending
168
171
  pendings.each do |pending|
@@ -227,6 +230,9 @@ module HTTPX
227
230
  consume
228
231
  end
229
232
  nil
233
+ rescue IOError => e
234
+ @write_buffer.clear
235
+ on_io_error(e)
230
236
  rescue StandardError => e
231
237
  @write_buffer.clear
232
238
  on_error(e)
@@ -256,16 +262,17 @@ module HTTPX
256
262
  # bypasses state machine rules while setting the connection in the
257
263
  # :closed state.
258
264
  def force_close(delete_pending = false)
265
+ force_purge
266
+ return unless @state == :closed
267
+
259
268
  if delete_pending
260
269
  @pending.clear
261
270
  elsif (parser = @parser)
262
271
  enqueue_pending_requests_from_parser(parser)
263
272
  end
264
- return if @state == :closed
265
273
 
266
- @state = :closed
267
- @write_buffer.clear
268
- purge_after_closed
274
+ return unless @pending.empty?
275
+
269
276
  disconnect
270
277
  emit(:force_closed, delete_pending)
271
278
  end
@@ -281,6 +288,19 @@ module HTTPX
281
288
  def reset
282
289
  return if @state == :closing || @state == :closed
283
290
 
291
+ # do not reset a connection which may have restarted back to :idle, such when the parser resets
292
+ # (example: HTTP/1 parser disabling pipelining)
293
+ return if @state == :idle && @pending.any?
294
+
295
+ parser = @parser
296
+
297
+ if parser && parser.respond_to?(:max_concurrent_requests)
298
+ # if connection being reset has at some downgraded the number of concurrent
299
+ # requests, such as in the case where an attempt to use HTTP/1 pipelining failed,
300
+ # keep that information around.
301
+ @max_concurrent_requests = parser.max_concurrent_requests
302
+ end
303
+
284
304
  transition(:closing)
285
305
 
286
306
  transition(:closed)
@@ -298,8 +318,8 @@ module HTTPX
298
318
  log(level: 3) { "keep alive timeout expired, pinging connection..." }
299
319
  @pending << request
300
320
  transition(:active) if @state == :inactive
301
- parser.ping
302
321
  request.ping!
322
+ ping
303
323
  return
304
324
  end
305
325
 
@@ -323,7 +343,10 @@ module HTTPX
323
343
  purge_after_closed
324
344
  @write_buffer.clear
325
345
  transition(:idle)
326
- @parser = nil if @parser
346
+ return unless @parser
347
+
348
+ enqueue_pending_requests_from_parser(parser)
349
+ @parser = nil
327
350
  end
328
351
 
329
352
  def used?
@@ -375,6 +398,19 @@ module HTTPX
375
398
  current_session.deselect_connection(self, current_selector, @cloned)
376
399
  end
377
400
 
401
+ def on_connect_error(e)
402
+ # connect errors, exit gracefully
403
+ error = ConnectionError.new(e.message)
404
+ error.set_backtrace(e.backtrace)
405
+ handle_connect_error(error) if connecting?
406
+ force_close
407
+ end
408
+
409
+ def on_io_error(e)
410
+ on_error(e)
411
+ force_close(true)
412
+ end
413
+
378
414
  def on_error(error, request = nil)
379
415
  if error.is_a?(OperationTimeoutError)
380
416
 
@@ -424,11 +460,11 @@ module HTTPX
424
460
  #
425
461
  # this condition takes into account:
426
462
  #
427
- # * the number of inflight requests
428
463
  # * the number of pending requests
464
+ # * the number of inflight requests
429
465
  # * whether the write buffer has bytes (i.e. for close handshake)
430
466
  if @pending.empty? && @inflight.zero? && @write_buffer.empty?
431
- log(level: 3) { "NO MORE REQUESTS..." } if @parser && @parser.pending.any?
467
+ no_more_requests_loop_check if @parser && @parser.pending.any?
432
468
 
433
469
  # terminate if an altsvc connection has been established
434
470
  terminate if @altsvc_connection
@@ -478,7 +514,7 @@ module HTTPX
478
514
 
479
515
  # exit #consume altogether if all outstanding requests have been dealt with
480
516
  if @pending.empty? && @inflight.zero? && @write_buffer.empty? # rubocop:disable Style/Next
481
- log(level: 3) { "NO MORE REQUESTS..." } if @parser && @parser.pending.any?
517
+ no_more_requests_loop_check if @parser && @parser.pending.any?
482
518
 
483
519
  # terminate if an altsvc connection has been established
484
520
  terminate if @altsvc_connection
@@ -493,7 +529,7 @@ module HTTPX
493
529
  # flush as many bytes as the sockets allow.
494
530
  #
495
531
  loop do
496
- # buffer has been drainned, mark and exit the write loop.
532
+ # buffer has been drained, mark and exit the write loop.
497
533
  if @write_buffer.empty?
498
534
  # we only mark as drained on the first loop
499
535
  write_drained = write_drained.nil? && @inflight.positive?
@@ -578,6 +614,7 @@ module HTTPX
578
614
  end
579
615
 
580
616
  def enqueue_pending_requests_from_parser(parser)
617
+ parser.reset_requests # move sequential requests back to pending queue.
581
618
  parser_pending_requests = parser.pending
582
619
 
583
620
  return if parser_pending_requests.empty?
@@ -586,11 +623,14 @@ module HTTPX
586
623
  # back to the pending list before the parser is reset.
587
624
  @inflight -= parser_pending_requests.size
588
625
  @pending.unshift(*parser_pending_requests)
626
+
627
+ parser.pending.clear
589
628
  end
590
629
 
591
630
  def build_parser(protocol = @io.protocol)
592
631
  parser = parser_type(protocol).new(@write_buffer, @options)
593
632
  set_parser_callbacks(parser)
633
+ parser.max_concurrent_requests = @max_concurrent_requests if @max_concurrent_requests && parser.respond_to?(:max_concurrent_requests=)
594
634
  parser
595
635
  end
596
636
 
@@ -600,6 +640,7 @@ module HTTPX
600
640
  build_altsvc_connection(alt_origin, origin, alt_params)
601
641
  end
602
642
  @response_received_at = Utils.now
643
+ @no_more_requests_counter = 0
603
644
  @inflight -= 1
604
645
  response.finish!
605
646
  request.emit(:response, response)
@@ -608,7 +649,7 @@ module HTTPX
608
649
  build_altsvc_connection(alt_origin, origin, alt_params)
609
650
  end
610
651
 
611
- parser.on(:pong, &method(:send_pending))
652
+ parser.on(:pong, &method(:pong))
612
653
 
613
654
  parser.on(:promise) do |request, stream|
614
655
  request.emit(:promise, parser, stream)
@@ -627,7 +668,6 @@ module HTTPX
627
668
  end
628
669
  parser.on(:close) do
629
670
  reset
630
- disconnect
631
671
  end
632
672
  parser.on(:close_handshake) do
633
673
  consume unless @state == :closed
@@ -684,11 +724,7 @@ module HTTPX
684
724
  Errno::ENOENT,
685
725
  SocketError,
686
726
  IOError => e
687
- # connect errors, exit gracefully
688
- error = ConnectionError.new(e.message)
689
- error.set_backtrace(e.backtrace)
690
- handle_connect_error(error) if connecting?
691
- force_close
727
+ on_connect_error(e)
692
728
  rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
693
729
  # connect errors, exit gracefully
694
730
  handle_error(e)
@@ -719,29 +755,17 @@ module HTTPX
719
755
  when :inactive
720
756
  return unless @state == :open
721
757
 
758
+ # @type ivar @parser: HTTP1 | HTTP2
759
+
722
760
  # do not deactivate connection in use
723
761
  return if @inflight.positive? || @parser.waiting_for_ping?
724
-
725
- disconnect
726
762
  when :closing
727
763
  return unless connecting? || @state == :open
728
-
729
- unless @write_buffer.empty?
730
- # preset state before handshake, as error callbacks
731
- # may take it back here.
732
- @state = nextstate
733
- # handshakes, try sending
734
- consume
735
- @write_buffer.clear
736
- return
737
- end
738
764
  when :closed
739
765
  return unless @state == :closing
740
766
  return unless @write_buffer.empty?
741
767
 
742
768
  purge_after_closed
743
- disconnect if @pending.empty?
744
-
745
769
  when :already_open
746
770
  nextstate = :open
747
771
  # the first check for given io readiness must still use a timeout.
@@ -758,19 +782,51 @@ module HTTPX
758
782
  end
759
783
  log(level: 3) { "#{@state} -> #{nextstate}" }
760
784
  @state = nextstate
785
+ # post state change
786
+ case nextstate
787
+ when :inactive
788
+ disconnect
789
+ when :closing
790
+ return if @write_buffer.empty?
791
+
792
+ # try flushing termination handshakes
793
+ consume
794
+ @write_buffer.clear
795
+ when :closed
796
+ # TODO: should this raise an error instead?
797
+ return unless @pending.empty?
798
+
799
+ disconnect
800
+ end
801
+ end
802
+
803
+ def force_purge
804
+ return if @state == :closed
805
+
806
+ @state = :closed
807
+ @write_buffer.clear
808
+ begin
809
+ purge_after_closed
810
+ rescue IOError
811
+ # may be raised when closing the socket.
812
+ # due to connection reuse / fiber scheduling, it may
813
+ # have been reopened, to bail out in that case.
814
+ end
761
815
  end
762
816
 
763
817
  def close_sibling
764
- return unless @sibling
818
+ sibling = @sibling
765
819
 
766
- if @sibling.io_connected?
820
+ return unless sibling
821
+
822
+ if sibling.io_connected?
767
823
  reset
768
824
  # TODO: transition connection to closed
769
825
  end
770
826
 
771
- unless @sibling.state == :closed
772
- merge(@sibling) unless @main_sibling
773
- @sibling.force_reset(true)
827
+ unless sibling.state == :closed
828
+ merge(sibling) unless @main_sibling
829
+ sibling.force_reset(true)
774
830
  end
775
831
 
776
832
  @sibling = nil
@@ -850,28 +906,61 @@ module HTTPX
850
906
  end
851
907
  end
852
908
 
909
+ def ping
910
+ return if parser.waiting_for_ping?
911
+
912
+ parser.ping
913
+ call
914
+ end
915
+
916
+ def pong
917
+ @response_received_at = Utils.now
918
+ @no_more_requests_counter = 0
919
+ send_pending
920
+ end
921
+
922
+ def no_more_requests_loop_check
923
+ log(level: 3) { "NO MORE REQUESTS..." }
924
+ @no_more_requests_counter += 1
925
+
926
+ return if @no_more_requests_counter < 50
927
+
928
+ raise Error, "connection corrupted, aborted after looping for a while, " \
929
+ "please report this https://gitlab.com/os85/httpx/-/work_items " \
930
+ "along with debug logs"
931
+ end
932
+
853
933
  # recover internal state and emit all relevant error responses when +error+ was raised.
854
934
  # this takes an optiona +request+ which may have already been handled and can be opted out
855
935
  # in the state recovery process.
856
936
  def handle_error(error, request = nil)
857
- parser.handle_error(error, request) if @parser && @parser.respond_to?(:handle_error)
858
- while (req = @pending.shift)
859
- next if request && req == request
937
+ if request
938
+ @inflight -= 1
939
+ response = ErrorResponse.new(request, error)
940
+ request.response = response
941
+ request.emit(:response, response)
942
+ end
943
+
944
+ pending = @pending
945
+ if (parser = @parser) && parser.respond_to?(:handle_error)
946
+ # parser.handle_error may disconnect the connection
947
+ pending = @pending.dup
948
+ @pending = []
860
949
 
861
- response = ErrorResponse.new(req, error)
862
- req.response = response
863
- req.emit(:response, response)
950
+ parser.handle_error(error, request)
864
951
  end
865
952
 
866
- return unless request
953
+ while (req = pending.shift)
954
+ next if request && req == request
867
955
 
868
- @inflight -= 1
869
- response = ErrorResponse.new(request, error)
870
- request.response = response
871
- request.emit(:response, response)
956
+ resp = ErrorResponse.new(req, error)
957
+ req.response = resp
958
+ req.emit(:response, resp)
959
+ end
872
960
  end
873
961
 
874
962
  def set_request_timeouts(request)
963
+ request.connection = self
875
964
  set_request_write_timeout(request)
876
965
  set_request_read_timeout(request)
877
966
  set_request_request_timeout(request)
@@ -907,29 +996,29 @@ module HTTPX
907
996
  end
908
997
  end
909
998
 
910
- def write_timeout_callback(request, write_timeout)
999
+ def write_timeout_callback(request, timeout)
911
1000
  return if request.state == :done
912
1001
 
913
1002
  @write_buffer.clear
914
- error = WriteTimeoutError.new(request, nil, write_timeout)
1003
+ error = WriteTimeoutError.new(request, nil, timeout)
915
1004
 
916
- on_error(error, request)
1005
+ request.handle_error(error)
917
1006
  end
918
1007
 
919
- def read_timeout_callback(request, read_timeout, error_type = ReadTimeoutError)
1008
+ def read_timeout_callback(request, timeout, error_type = ReadTimeoutError)
920
1009
  response = request.response
921
1010
 
922
1011
  return if response && response.finished?
923
1012
 
924
1013
  @write_buffer.clear
925
- error = error_type.new(request, request.response, read_timeout)
1014
+ error = error_type.new(request, response, timeout)
926
1015
 
927
- on_error(error, request)
1016
+ request.handle_error(error)
928
1017
  end
929
1018
 
930
1019
  def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
931
1020
  request.set_timeout_callback(start_event) do
932
- unless @current_selector
1021
+ unless (selector = @current_selector)
933
1022
  raise Error, "request has been resend to an out-of-session connection, and this " \
934
1023
  "should never happen!!! Please report this error! " \
935
1024
  "(state:#{@state}, " \
@@ -940,7 +1029,7 @@ module HTTPX
940
1029
  "coalesced?:#{coalesced?})"
941
1030
  end
942
1031
 
943
- timer = @current_selector.after(timeout, callback)
1032
+ timer = selector.after(timeout, callback)
944
1033
  request.active_timeouts << label
945
1034
 
946
1035
  Array(finish_events).each do |event|
data/lib/httpx/io/ssl.rb CHANGED
@@ -51,6 +51,7 @@ module HTTPX
51
51
  end
52
52
 
53
53
  def protocol
54
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
54
55
  @io.alpn_protocol || super
55
56
  rescue StandardError
56
57
  super
@@ -60,6 +61,7 @@ module HTTPX
60
61
  # in jruby, alpn_protocol may return ""
61
62
  # https://github.com/jruby/jruby-openssl/issues/287
62
63
  def protocol
64
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
63
65
  proto = @io.alpn_protocol
64
66
 
65
67
  return super if proto.nil? || proto.empty?
@@ -76,9 +78,10 @@ module HTTPX
76
78
 
77
79
  def verify_hostname(host)
78
80
  return false if @ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
79
- return false if !@io.respond_to?(:peer_cert) || @io.peer_cert.nil?
81
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
82
+ return false if !@io.respond_to?(:peer_cert) || (peer_cert = @io.peer_cert).nil?
80
83
 
81
- OpenSSL::SSL.verify_certificate_identity(@io.peer_cert, host)
84
+ OpenSSL::SSL.verify_certificate_identity(peer_cert, host)
82
85
  end
83
86
 
84
87
  def connected?
@@ -86,7 +89,9 @@ module HTTPX
86
89
  end
87
90
 
88
91
  def ssl_session_expired?
89
- @ssl_session.nil? || Process.clock_gettime(Process::CLOCK_REALTIME) >= (@ssl_session.time.to_f + @ssl_session.timeout)
92
+ ssl_session = @ssl_session
93
+
94
+ ssl_session.nil? || Process.clock_gettime(Process::CLOCK_REALTIME) >= (ssl_session.time.to_f + ssl_session.timeout)
90
95
  end
91
96
 
92
97
  def connect
@@ -97,6 +102,8 @@ module HTTPX
97
102
  return unless @state == :connected
98
103
  end
99
104
 
105
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
106
+
100
107
  unless @io.is_a?(OpenSSL::SSL::SSLSocket)
101
108
  if (hostname_is_ip = (@ip == @sni_hostname)) && @ctx.verify_hostname
102
109
  # IPv6 address would be "[::1]", must turn to "0000:0000:0000:0000:0000:0000:0000:0001" for cert SAN check
@@ -105,16 +112,19 @@ module HTTPX
105
112
  @ctx.verify_hostname = false
106
113
  end
107
114
 
108
- @io = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
115
+ ssl = OpenSSL::SSL::SSLSocket.new(@io, @ctx)
109
116
 
110
- @io.hostname = @sni_hostname unless hostname_is_ip
111
- @io.session = @ssl_session unless ssl_session_expired?
112
- @io.sync_close = true
117
+ ssl.hostname = @sni_hostname unless hostname_is_ip
118
+ ssl.session = @ssl_session unless ssl_session_expired?
119
+ ssl.sync_close = true
120
+
121
+ @io = ssl
113
122
  end
114
123
  try_ssl_connect
115
124
  end
116
125
 
117
126
  def try_ssl_connect
127
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
118
128
  ret = @io.connect_nonblock(exception: false)
119
129
  log(level: 3, color: :cyan) { "TLS CONNECT: #{ret}..." }
120
130
  case ret
@@ -147,7 +157,9 @@ module HTTPX
147
157
  def log_transition_state(nextstate)
148
158
  return super unless nextstate == :negotiated
149
159
 
150
- server_cert = @io.peer_cert
160
+ # @type ivar @io: OpenSSL::SSL::SSLSocket
161
+
162
+ server_cert = @io.peer_cert #: OpenSSL::X509::Certificate
151
163
 
152
164
  "#{super}\n\n" \
153
165
  "SSL connection using #{@io.ssl_version} / #{Array(@io.cipher).first}\n" \
data/lib/httpx/io/tcp.rb CHANGED
@@ -23,18 +23,21 @@ module HTTPX
23
23
  @fallback_protocol = @options.fallback_protocol
24
24
  @port = origin.port
25
25
  @interests = :w
26
- if @options.io
27
- @io = case @options.io
28
- when Hash
29
- @options.io[origin.authority]
30
- else
31
- @options.io
32
- end
33
- raise Error, "Given IO objects do not match the request authority" unless @io
34
-
35
- _, _, _, ip = @io.addr
36
- @ip = Resolver::Entry.new(ip)
37
- @addresses << @ip
26
+ if (io = @options.io)
27
+ io =
28
+ case io
29
+ when Hash
30
+ io[origin.authority]
31
+ else
32
+ io
33
+ end
34
+ raise Error, "Given IO objects do not match the request authority" unless io
35
+
36
+ # @type var io: TCPSocket | OpenSSL::SSL::SSLSocket
37
+
38
+ _, _, _, ip = io.addr
39
+ @io = io
40
+ @addresses << (@ip = Resolver::Entry.new(ip))
38
41
  @keep_open = true
39
42
  @state = :connected
40
43
  else
@@ -183,6 +186,9 @@ module HTTPX
183
186
 
184
187
  begin
185
188
  @io.close
189
+ rescue StandardError => e
190
+ log { "error closing socket" }
191
+ log { e.full_message(highlight: false) }
186
192
  ensure
187
193
  transition(:closed)
188
194
  end
data/lib/httpx/io/unix.rb CHANGED
@@ -14,16 +14,20 @@ module HTTPX
14
14
  @state = :idle
15
15
  @options = options
16
16
  @fallback_protocol = @options.fallback_protocol
17
- if @options.io
18
- @io = case @options.io
19
- when Hash
20
- @options.io[origin.authority]
21
- else
22
- @options.io
23
- end
24
- raise Error, "Given IO objects do not match the request authority" unless @io
17
+ if (io = @options.io)
18
+ io =
19
+ case io
20
+ when Hash
21
+ io[origin.authority]
22
+ else
23
+ io
24
+ end
25
+ raise Error, "Given IO objects do not match the request authority" unless io
26
+
27
+ # @type var io: UNIXSocket
25
28
 
26
- @path = @io.path
29
+ _, @path = io.addr
30
+ @io = io
27
31
  @keep_open = true
28
32
  @state = :connected
29
33
  elsif path
data/lib/httpx/options.rb CHANGED
@@ -202,16 +202,19 @@ module HTTPX
202
202
 
203
203
  REQUEST_BODY_IVARS = %i[@headers].freeze
204
204
 
205
- def ==(other)
206
- super || options_equals?(other)
207
- end
205
+ # checks whether +other+ matches the same connection-level options
206
+ def connection_options_match?(other, ignore_ivars = nil)
207
+ return true if self == other
208
208
 
209
- # checks whether +other+ is equal by comparing the session options
210
- def options_equals?(other, ignore_ivars = REQUEST_BODY_IVARS)
211
209
  # headers and other request options do not play a role, as they are
212
210
  # relevant only for the request.
213
- ivars = instance_variables - ignore_ivars
214
- other_ivars = other.instance_variables - ignore_ivars
211
+ ivars = instance_variables
212
+ ivars.reject! { |iv| REQUEST_BODY_IVARS.include?(iv) }
213
+ ivars.reject! { |iv| ignore_ivars.include?(iv) } if ignore_ivars
214
+
215
+ other_ivars = other.instance_variables
216
+ other_ivars.reject! { |iv| REQUEST_BODY_IVARS.include?(iv) }
217
+ other_ivars.reject! { |iv| ignore_ivars.include?(iv) } if ignore_ivars
215
218
 
216
219
  return false if ivars.size != other_ivars.size
217
220
 
@@ -222,6 +225,19 @@ module HTTPX
222
225
  end
223
226
  end
224
227
 
228
+ RESOLVER_IVARS = %i[
229
+ @resolver_class @resolver_cache @resolver_options
230
+ @resolver_native_class @resolver_system_class @resolver_https_class
231
+ ].freeze
232
+
233
+ # checks whether +other+ matches the same resolver-level options
234
+ def resolver_options_match?(other)
235
+ self == other ||
236
+ RESOLVER_IVARS.all? do |ivar|
237
+ instance_variable_get(ivar) == other.instance_variable_get(ivar)
238
+ end
239
+ end
240
+
225
241
  # returns a HTTPX::Options instance resulting of the merging of +other+ with self.
226
242
  # it may return self if +other+ is self or equal to self.
227
243
  def merge(other)