httpx 1.7.4 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 125df778093197c9a1fe8872211cf4085a5a885a40536b58501ce1938f8d28ee
4
- data.tar.gz: 5a17b5b02212f65ba3472f4dfda96a017fc1ef9a4fb7a3dce85d7b4ac6a8c6bb
3
+ metadata.gz: b34abe93b42862837d9131524d6e9663a93474f70c65ac2a10946eb62c9aeb14
4
+ data.tar.gz: '0853b845878005ace796106f79b735d9d75e205b7ab28c03a3786b53233d6a4b'
5
5
  SHA512:
6
- metadata.gz: b6a4014db970d25a1ac51ea73ac5bd828bf8c0f1012ec778abdf054d9dd6dcfe6cf7fc5f73f915a39d74e4e420329e1293e020d63955601e6f669ae6164a016b
7
- data.tar.gz: 96b98469fe6194bbd3427adfd1bfc6032be9c44f2c68ff301a6da20a90741b1979aa194c113a11e1b82cb9a54c3559ef7c35b908d41c10297746e438b557eca3
6
+ metadata.gz: 0be425ba5468990b31fb3fe04b97ee8c727cc1f16690decc2057314c3645e5baa21fae9b54585bc6e628410374dbfd9b8676df3aaed180b321a9b59cbbad2853
7
+ data.tar.gz: 3cbe9b0e2952c7d330bffe727f2b478f53152d431a306d48f5f41cb5033e9ba7fa2ddec9a319aed49f051c8e9e6e2d8e489ecccc83a6c622246ef6922c9bbecc
@@ -17,7 +17,7 @@ You can pass chain several tracers, and callbacks will be relayed to all of them
17
17
  HTTP.plugin(:tracing).with(tracer: telemetry_platform_tracer).with(tracer: telemetry2_platform_tracer)
18
18
  ```
19
19
 
20
- This was developed to be the foundation on top of which the datadag and OTel integrations will be built.
20
+ This was developed to be the foundation on top of which the datadog and OTel integrations will be built.
21
21
 
22
22
  ## Improvements
23
23
 
@@ -0,0 +1,10 @@
1
+ # 1.7.5
2
+
3
+ ## Improvements
4
+
5
+ * `:tracing` plugin: make `Request#init_time` a UTC timestamp.
6
+
7
+ ## Bugfixes
8
+
9
+ * fixed handling of conditional responses which was making a batch of concurrent requests being handled serially after they failed.
10
+ * `datadog` adapter: use `Request#init_time` as the span start time, which will fix the bug where the span wasn't including the time it takes to open the connection (TCP/TLS handhshakes).
@@ -0,0 +1,24 @@
1
+ # 1.7.6
2
+
3
+ ## Improvements
4
+
5
+ * `datadog` adapter: support setting custom `peer.service`.
6
+ * stopped doing `Thread.pass` after checking connections back into the pool. The goal of this was to allow threads potentially waiting on a connection for the same origin to have the opportunity to take it before the same thread could check the same connection out, thereby preventing starvation on limited pool sizes; this however backfired for the common case of unbounded pool sizes (the default), and was specially bad when used for multiple concurrent requests on multiple origins, where avoidable lag was introduced on the whole processing every time a connection was checked back in. Thereby, the initial problem needs to be solved at the mutex implementation of CRuby.
7
+ * connection: after keep alive timeout expired, try writing `PING` frame to the socket as soon as it's emitted (previously, the frame was being buffered and the socket would be probed for readiness before writing it, whereas in most cases this is avoidable).
8
+
9
+
10
+ ## Bugfixes
11
+
12
+ * reuse the same resolver instance when resolving multiple concurrent requests in the same option (it was previously instantiating different ones based on option heuristics, such as when requests had different `:origin`).
13
+ * on successful early name resolution (from cache or hosts file, for example), check the resolver back to the pool (the object was being dereferenced).
14
+ * http1 parser: turn buffer back into a string on reset, instead of preserving its current type (in cases where it was a chunked transfer decoder and connection was kept-alive, it was keeping it around and using it to parse the next request, which broke).
15
+ * http1 parser: enable pipelining based on the max concurrent requests, which the connection may have had pre-set to 1 based on prior knowledge of origin or other heuristics.
16
+ * http1 parser: when disabling pipelining, mark incomplete inlined requests as idle before sending them back to the pending queue.
17
+ * http1 and http2 parser: remove references to requests escalating an error; in scenarios of connection reuse (such as when using the `:persistent` plugin), these requests could be, on a transition to idle state, resent even in the absence of an external session reference, causing avoidable overhead or contention leading to timeouts or errors.
18
+ * connection: rescue and ignore exceptions when calling `OpenSSL::SSL::SSLSocket#close` or `Socket#close`.
19
+ * connection: when sending multiple requests into an open HTTP/2 connection which keep alive may have expired, only a single (instead of multiple, as before) `PING` frame will be sent (in the prior state, only the first `PING` was being acknowledged, which made the connection accumulate unacknowledged ping payloads).
20
+ * connection: on handling errors, preventing potential race condition by copying pending requests into a local var before calling `parser.handle_error` (this may trigger a callback which calls `#reset`, which may call `#disconnect`, which would check the connection back into the pool and make it available for another thread, which could change the state of the ivar containing pending requests).
21
+ * connection: skip resetting a connection when idle and having pending requests; this may happen in situation where parsers may have reset-and-back-to-idle the connection to resend failed requests as protocol (such as in the case of failed HTTP/1 pipelinining requests where incomplete requests are to be resent to the connection once pipelining is disabled).
22
+ * connection: raise request timeout errors for the request the connection is currently in, instead of the connection where it was initially sent on and set timeouts on; this happens in situations where the request may be retried and sent to a different connection, which forces the request to keep a reference to the connection it's currently in (and discard it when it no longer needs it to prevent it from being GC'ed).
23
+ * `:auth` plugin: when used alongside the `:retries` plugin for multiple concurrent requests, it was calling the dynamic token generator block once for each request; fixed to call the block only once per retry window, so retrying requests will reuse the same.
24
+ * `:retries` plugin: retry request immediately if `:retry_after` is negative (while the option is validated for numbers, it can't control proc-based calculation).
@@ -19,6 +19,7 @@ module Datadog::Tracing
19
19
  Datadog::Tracing::Contrib::Ext::Metadata::TAG_BASE_SERVICE
20
20
  end
21
21
  TAG_PEER_HOSTNAME = Datadog::Tracing::Metadata::Ext::TAG_PEER_HOSTNAME
22
+ TAG_PEER_SERVICE = Datadog::Tracing::Metadata::Ext::TAG_PEER_SERVICE
22
23
 
23
24
  TAG_KIND = Datadog::Tracing::Metadata::Ext::TAG_KIND
24
25
  TAG_CLIENT = Datadog::Tracing::Metadata::Ext::SpanKind::TAG_CLIENT
@@ -51,7 +52,7 @@ module Datadog::Tracing
51
52
  end
52
53
 
53
54
  def start(request)
54
- request.datadog_span = initialize_span(request, now)
55
+ request.datadog_span = initialize_span(request, request.init_time)
55
56
  end
56
57
 
57
58
  def reset(request)
@@ -108,7 +109,9 @@ module Datadog::Tracing
108
109
  span.set_tag(TAG_PEER_HOSTNAME, uri.host)
109
110
 
110
111
  # Tag as an external peer service
111
- # span.set_tag(TAG_PEER_SERVICE, span.service)
112
+ if (peer_service = config[:peer_service])
113
+ span.set_tag(TAG_PEER_SERVICE, peer_service)
114
+ end
112
115
 
113
116
  if config[:distributed_tracing]
114
117
  propagate_trace_http(
@@ -132,10 +135,6 @@ module Datadog::Tracing
132
135
  Datadog.logger.error(e.backtrace)
133
136
  end
134
137
 
135
- def now
136
- ::Datadog::Core::Utils::Time.now.utc
137
- end
138
-
139
138
  def configuration(request)
140
139
  Datadog.configuration.tracing[:httpx, request.uri.host]
141
140
  end
@@ -214,6 +213,11 @@ module Datadog::Tracing
214
213
  o.env "DD_TRACE_HTTPX_ANALYTICS_SAMPLE_RATE"
215
214
  o.default 1.0
216
215
  end
216
+
217
+ option :peer_service do |o|
218
+ o.type :string, nilable: true
219
+ o.env "DD_TRACE_HTTPX_PEER_SERVICE"
220
+ end
217
221
  else
218
222
  option :enabled do |o|
219
223
  o.default { env_to_bool("DD_TRACE_HTTPX_ENABLED", true) }
@@ -229,6 +233,11 @@ module Datadog::Tracing
229
233
  o.default { env_to_float(%w[DD_TRACE_HTTPX_ANALYTICS_SAMPLE_RATE DD_HTTPX_ANALYTICS_SAMPLE_RATE], 1.0) }
230
234
  o.lazy
231
235
  end
236
+
237
+ option :peer_service do |o|
238
+ o.default { env_to_string("DD_TRACE_HTTPX_PEER_SERVICE", nil) }
239
+ o.lazy
240
+ end
232
241
  end
233
242
 
234
243
  if defined?(Datadog::Tracing::Contrib::SpanAttributeSchema)
data/lib/httpx/altsvc.rb CHANGED
@@ -10,6 +10,8 @@ module HTTPX
10
10
 
11
11
  H2_ALTSVC_SCHEMES = %w[https h2].freeze
12
12
 
13
+ ALTSVC_IGNORE_IVARS = %i[@ssl].freeze
14
+
13
15
  def send(request)
14
16
  request.headers["alt-used"] = @origin.authority if @parser && !@write_buffer.full? && match_altsvcs?(request.uri)
15
17
 
@@ -35,11 +37,11 @@ module HTTPX
35
37
  end
36
38
 
37
39
  def match_altsvc_options?(uri, options)
38
- return @options == options unless @options.ssl.all? do |k, v|
40
+ return @options.connection_options_match?(options) unless @options.ssl.all? do |k, v|
39
41
  v == (k == :hostname ? uri.host : options.ssl[k])
40
42
  end
41
43
 
42
- @options.options_equals?(options, Options::REQUEST_BODY_IVARS + %i[@ssl])
44
+ @options.connection_options_match?(options, ALTSVC_IGNORE_IVARS)
43
45
  end
44
46
 
45
47
  def altsvc_match?(uri, other_uri)
@@ -28,7 +28,8 @@ module HTTPX
28
28
  @version = [1, 1]
29
29
  @pending = []
30
30
  @requests = []
31
- @handshake_completed = false
31
+ @request = nil
32
+ @handshake_completed = @pipelining = false
32
33
  end
33
34
 
34
35
  def timeout
@@ -53,7 +54,9 @@ module HTTPX
53
54
  end
54
55
 
55
56
  def reset_requests
56
- @pending.unshift(*@requests)
57
+ requests = @requests
58
+ requests.each { |r| r.transition(:idle) }
59
+ @pending.unshift(*requests)
57
60
  @requests.clear
58
61
  end
59
62
 
@@ -90,7 +93,7 @@ module HTTPX
90
93
  return if @requests.include?(request)
91
94
 
92
95
  @requests << request
93
- @pipelining = true if @requests.size > 1
96
+ @pipelining = @max_concurrent_requests > 1 && @requests.size > 1
94
97
  end
95
98
 
96
99
  def consume
@@ -145,15 +148,17 @@ module HTTPX
145
148
 
146
149
  return unless request
147
150
 
148
- log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
149
- log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
150
- response = request.response
151
+ begin
152
+ log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
153
+ log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
154
+ response = request.response
151
155
 
152
- response << chunk
153
- rescue StandardError => e
154
- error_response = ErrorResponse.new(request, e)
155
- request.response = error_response
156
- dispatch
156
+ response << chunk
157
+ rescue StandardError => e
158
+ error_response = ErrorResponse.new(request, e)
159
+ request.response = error_response
160
+ dispatch(request)
161
+ end
157
162
  end
158
163
 
159
164
  def on_complete
@@ -162,12 +167,10 @@ module HTTPX
162
167
  return unless request
163
168
 
164
169
  log(level: 2) { "parsing complete" }
165
- dispatch
170
+ dispatch(request)
166
171
  end
167
172
 
168
- def dispatch
169
- request = @request
170
-
173
+ def dispatch(request)
171
174
  if request.expects?
172
175
  @parser.reset!
173
176
  return handle(request)
@@ -217,12 +220,12 @@ module HTTPX
217
220
  if @pipelining
218
221
  catch(:called) { disable }
219
222
  else
220
- @requests.each do |req|
223
+ while (req = @requests.shift)
221
224
  next if request && request == req
222
225
 
223
226
  emit(:error, req, ex)
224
227
  end
225
- @pending.each do |req|
228
+ while (req = @pending.shift)
226
229
  next if request && request == req
227
230
 
228
231
  emit(:error, req, ex)
@@ -57,29 +57,37 @@ module HTTPX
57
57
 
58
58
  return if @buffer.empty?
59
59
 
60
+ # HTTP/2 GOAWAY frame buffered.
60
61
  return :w
61
62
  end
62
63
 
63
64
  unless @connection.state == :connected && @handshake_completed
65
+ # HTTP/2 in intermediate state or still completing initialization-
64
66
  return @buffer.empty? ? :r : :rw
65
67
  end
66
68
 
67
69
  unless @connection.send_buffer.empty?
70
+ # HTTP/2 connection is buffering data chunks and failing to emit DATA frames,
71
+ # most likely because the flow control window is exhausted.
68
72
  return :rw unless @buffer.empty?
69
73
 
70
74
  # waiting for WINDOW_UPDATE frames
71
75
  return :r
72
76
  end
73
77
 
78
+ # there are pending bufferable requests
74
79
  return :w if !@pending.empty? && can_buffer_more_requests?
75
80
 
81
+ # there are pending frames from the last run
76
82
  return :w unless @drains.empty?
77
83
 
78
84
  if @buffer.empty?
85
+ # skip if no more requests or pings to process
79
86
  return if @streams.empty? && @pings.empty?
80
87
 
81
88
  :r
82
89
  else
90
+ # buffered frames
83
91
  :w
84
92
  end
85
93
  end
@@ -138,7 +146,7 @@ module HTTPX
138
146
  settings_ex.set_backtrace(ex.backtrace)
139
147
  ex = settings_ex
140
148
  end
141
- @streams.each_key do |req|
149
+ while (req, _ = @streams.shift)
142
150
  next if request && request == req
143
151
 
144
152
  emit(:error, req, ex)
@@ -399,10 +407,9 @@ module HTTPX
399
407
  ex = GoawayError.new(error)
400
408
  ex.set_backtrace(caller)
401
409
 
402
- @pending.unshift(*@streams.keys)
410
+ handle_error(ex)
403
411
  teardown
404
412
 
405
- handle_error(ex)
406
413
  end
407
414
  end
408
415
  return unless is_connection_closed && @streams.empty?
@@ -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+.
@@ -287,6 +288,10 @@ module HTTPX
287
288
  def reset
288
289
  return if @state == :closing || @state == :closed
289
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
+
290
295
  parser = @parser
291
296
 
292
297
  if parser && parser.respond_to?(:max_concurrent_requests)
@@ -313,8 +318,8 @@ module HTTPX
313
318
  log(level: 3) { "keep alive timeout expired, pinging connection..." }
314
319
  @pending << request
315
320
  transition(:active) if @state == :inactive
316
- parser.ping
317
321
  request.ping!
322
+ ping
318
323
  return
319
324
  end
320
325
 
@@ -455,11 +460,11 @@ module HTTPX
455
460
  #
456
461
  # this condition takes into account:
457
462
  #
458
- # * the number of inflight requests
459
463
  # * the number of pending requests
464
+ # * the number of inflight requests
460
465
  # * whether the write buffer has bytes (i.e. for close handshake)
461
466
  if @pending.empty? && @inflight.zero? && @write_buffer.empty?
462
- log(level: 3) { "NO MORE REQUESTS..." } if @parser && @parser.pending.any?
467
+ no_more_requests_loop_check if @parser && @parser.pending.any?
463
468
 
464
469
  # terminate if an altsvc connection has been established
465
470
  terminate if @altsvc_connection
@@ -509,7 +514,7 @@ module HTTPX
509
514
 
510
515
  # exit #consume altogether if all outstanding requests have been dealt with
511
516
  if @pending.empty? && @inflight.zero? && @write_buffer.empty? # rubocop:disable Style/Next
512
- log(level: 3) { "NO MORE REQUESTS..." } if @parser && @parser.pending.any?
517
+ no_more_requests_loop_check if @parser && @parser.pending.any?
513
518
 
514
519
  # terminate if an altsvc connection has been established
515
520
  terminate if @altsvc_connection
@@ -635,6 +640,7 @@ module HTTPX
635
640
  build_altsvc_connection(alt_origin, origin, alt_params)
636
641
  end
637
642
  @response_received_at = Utils.now
643
+ @no_more_requests_counter = 0
638
644
  @inflight -= 1
639
645
  response.finish!
640
646
  request.emit(:response, response)
@@ -643,7 +649,7 @@ module HTTPX
643
649
  build_altsvc_connection(alt_origin, origin, alt_params)
644
650
  end
645
651
 
646
- parser.on(:pong, &method(:send_pending))
652
+ parser.on(:pong, &method(:pong))
647
653
 
648
654
  parser.on(:promise) do |request, stream|
649
655
  request.emit(:promise, parser, stream)
@@ -755,16 +761,6 @@ module HTTPX
755
761
  return if @inflight.positive? || @parser.waiting_for_ping?
756
762
  when :closing
757
763
  return unless connecting? || @state == :open
758
-
759
- unless @write_buffer.empty?
760
- # preset state before handshake, as error callbacks
761
- # may take it back here.
762
- @state = nextstate
763
- # handshakes, try sending
764
- consume
765
- @write_buffer.clear
766
- return
767
- end
768
764
  when :closed
769
765
  return unless @state == :closing
770
766
  return unless @write_buffer.empty?
@@ -790,6 +786,12 @@ module HTTPX
790
786
  case nextstate
791
787
  when :inactive
792
788
  disconnect
789
+ when :closing
790
+ return if @write_buffer.empty?
791
+
792
+ # try flushing termination handshakes
793
+ consume
794
+ @write_buffer.clear
793
795
  when :closed
794
796
  # TODO: should this raise an error instead?
795
797
  return unless @pending.empty?
@@ -813,16 +815,18 @@ module HTTPX
813
815
  end
814
816
 
815
817
  def close_sibling
816
- return unless @sibling
818
+ sibling = @sibling
817
819
 
818
- if @sibling.io_connected?
820
+ return unless sibling
821
+
822
+ if sibling.io_connected?
819
823
  reset
820
824
  # TODO: transition connection to closed
821
825
  end
822
826
 
823
- unless @sibling.state == :closed
824
- merge(@sibling) unless @main_sibling
825
- @sibling.force_reset(true)
827
+ unless sibling.state == :closed
828
+ merge(sibling) unless @main_sibling
829
+ sibling.force_reset(true)
826
830
  end
827
831
 
828
832
  @sibling = nil
@@ -902,28 +906,61 @@ module HTTPX
902
906
  end
903
907
  end
904
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
+
905
933
  # recover internal state and emit all relevant error responses when +error+ was raised.
906
934
  # this takes an optiona +request+ which may have already been handled and can be opted out
907
935
  # in the state recovery process.
908
936
  def handle_error(error, request = nil)
909
- parser.handle_error(error, request) if @parser && @parser.respond_to?(:handle_error)
910
- while (req = @pending.shift)
911
- 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
912
943
 
913
- response = ErrorResponse.new(req, error)
914
- req.response = response
915
- req.emit(:response, response)
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 = []
949
+
950
+ parser.handle_error(error, request)
916
951
  end
917
952
 
918
- return unless request
953
+ while (req = pending.shift)
954
+ next if request && req == request
919
955
 
920
- @inflight -= 1
921
- response = ErrorResponse.new(request, error)
922
- request.response = response
923
- request.emit(:response, response)
956
+ resp = ErrorResponse.new(req, error)
957
+ req.response = resp
958
+ req.emit(:response, resp)
959
+ end
924
960
  end
925
961
 
926
962
  def set_request_timeouts(request)
963
+ request.connection = self
927
964
  set_request_write_timeout(request)
928
965
  set_request_read_timeout(request)
929
966
  set_request_request_timeout(request)
@@ -959,29 +996,29 @@ module HTTPX
959
996
  end
960
997
  end
961
998
 
962
- def write_timeout_callback(request, write_timeout)
999
+ def write_timeout_callback(request, timeout)
963
1000
  return if request.state == :done
964
1001
 
965
1002
  @write_buffer.clear
966
- error = WriteTimeoutError.new(request, nil, write_timeout)
1003
+ error = WriteTimeoutError.new(request, nil, timeout)
967
1004
 
968
- on_error(error, request)
1005
+ request.handle_error(error)
969
1006
  end
970
1007
 
971
- def read_timeout_callback(request, read_timeout, error_type = ReadTimeoutError)
1008
+ def read_timeout_callback(request, timeout, error_type = ReadTimeoutError)
972
1009
  response = request.response
973
1010
 
974
1011
  return if response && response.finished?
975
1012
 
976
1013
  @write_buffer.clear
977
- error = error_type.new(request, request.response, read_timeout)
1014
+ error = error_type.new(request, response, timeout)
978
1015
 
979
- on_error(error, request)
1016
+ request.handle_error(error)
980
1017
  end
981
1018
 
982
1019
  def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
983
1020
  request.set_timeout_callback(start_event) do
984
- unless @current_selector
1021
+ unless (selector = @current_selector)
985
1022
  raise Error, "request has been resend to an out-of-session connection, and this " \
986
1023
  "should never happen!!! Please report this error! " \
987
1024
  "(state:#{@state}, " \
@@ -992,7 +1029,7 @@ module HTTPX
992
1029
  "coalesced?:#{coalesced?})"
993
1030
  end
994
1031
 
995
- timer = @current_selector.after(timeout, callback)
1032
+ timer = selector.after(timeout, callback)
996
1033
  request.active_timeouts << label
997
1034
 
998
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