httpx 1.6.2 → 1.6.3

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_6_3.md +47 -0
  3. data/lib/httpx/adapters/sentry.rb +1 -1
  4. data/lib/httpx/connection/http1.rb +9 -9
  5. data/lib/httpx/connection/http2.rb +14 -15
  6. data/lib/httpx/connection.rb +115 -102
  7. data/lib/httpx/extensions.rb +0 -14
  8. data/lib/httpx/io/ssl.rb +1 -1
  9. data/lib/httpx/loggable.rb +12 -2
  10. data/lib/httpx/options.rb +20 -0
  11. data/lib/httpx/plugins/callbacks.rb +15 -1
  12. data/lib/httpx/plugins/digest_auth.rb +1 -1
  13. data/lib/httpx/plugins/proxy/http.rb +37 -9
  14. data/lib/httpx/plugins/response_cache/file_store.rb +1 -0
  15. data/lib/httpx/plugins/response_cache.rb +13 -2
  16. data/lib/httpx/plugins/stream_bidi.rb +15 -6
  17. data/lib/httpx/pool.rb +53 -19
  18. data/lib/httpx/request.rb +3 -13
  19. data/lib/httpx/resolver/https.rb +35 -19
  20. data/lib/httpx/resolver/multi.rb +8 -27
  21. data/lib/httpx/resolver/native.rb +46 -38
  22. data/lib/httpx/resolver/resolver.rb +45 -28
  23. data/lib/httpx/resolver/system.rb +63 -39
  24. data/lib/httpx/selector.rb +35 -20
  25. data/lib/httpx/session.rb +18 -28
  26. data/lib/httpx/transcoder/deflate.rb +13 -8
  27. data/lib/httpx/transcoder/utils/body_reader.rb +1 -2
  28. data/lib/httpx/transcoder/utils/deflater.rb +1 -2
  29. data/lib/httpx/version.rb +1 -1
  30. data/sig/connection.rbs +12 -3
  31. data/sig/loggable.rbs +5 -1
  32. data/sig/options.rbs +5 -1
  33. data/sig/plugins/callbacks.rbs +3 -0
  34. data/sig/plugins/stream_bidi.rbs +3 -5
  35. data/sig/resolver/https.rbs +2 -0
  36. data/sig/resolver/multi.rbs +0 -9
  37. data/sig/resolver/native.rbs +0 -2
  38. data/sig/resolver/resolver.rbs +9 -8
  39. data/sig/resolver/system.rbs +4 -2
  40. data/sig/selector.rbs +2 -0
  41. data/sig/session.rbs +5 -3
  42. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c039169fa319d4e42cff7cb34d8a9cc7fc571e62565f673c32436645780222c
4
- data.tar.gz: 6affb65b4fa71aa813cffc498034e0c6a6609bcdb0fca9d0ae9df738d4786d1d
3
+ metadata.gz: f49b29ea3703f6f40abe3cd82d455235b0a0b50a694bd8fa55839ac471d32bbb
4
+ data.tar.gz: ce933bb3c35d9434f810d4fc83acb244a1ad0db187123d5f01dcbbbd83622a47
5
5
  SHA512:
6
- metadata.gz: 21be43709d51f7c50d1624923bc1657c9f096b84686612ff70f660af320d71d94d28a52ccef05f27558d74f22e1fd307944ca8ddf8b6c74c0b322e83eaad9b19
7
- data.tar.gz: c551d2b32a71c5bbf099283b3577abe80d5005f702af5edefb79f445296c37c8fb9afb3d52b18db254a0c4ddd115d5f45953549f9b3c99c90ff8b86aedd6cb91
6
+ metadata.gz: 258cb32129840347a1a37633fb6273133187c98636b63ffcc0b8064f39e3b5642d693ee5da5d7776529625fbe4b68b5193b405c2e0658b2c9b1193b49e316bdc
7
+ data.tar.gz: 387397ab1954b6abf6a8a54d8764fb25e786fb1bd293401e8afd57f823464f67210527fee0b9e94b34cc3d2931143a5d94598fb6241f5728df77a8f1e045a5bf
@@ -0,0 +1,47 @@
1
+ # 1.6.3
2
+
3
+ ## Features
4
+
5
+ * allow redacting only headers, or only the body, when using `debug_redact: :headers` or `debug_redact: :body` respectively.
6
+
7
+ ## Improvements
8
+
9
+ * `system` resolver now works in a non-blocking manner, initiating the dns query in a separate thread and waiting on the pipe after that (it was blocking the main thread during resolution before).
10
+ * reduce allocation to a sinfle shared option object when headers are passed as a session-level option, like `HTTPX.with(headers: geaders).get(...)`
11
+ * privilege using `String#replace` in buffer operations (instead of "clean-then-append").
12
+ * using `Array#unshift` instead of `Array#concat` in order to ensure that request ordering is respected in the face of an in-between error which requires reconnect-and-resend.
13
+ * replaced more internal callback indirection with plain method calls.
14
+
15
+
16
+ ## Bugfixes
17
+
18
+ * https: prevent modification of the ssl context object when performing a reconnection.
19
+ * compression: do not return early if the decompression buffer yields an empty string (more frequent under jruby 10).
20
+ * response cache: take query params into account when caching or retrieving cached responses.
21
+ * response cache: do not decompress cached responses on body consumption (the response bodies are cached in plaintext).
22
+ * native resolver: pick next timeout associated with the hostname being resolved (and not the hostnames in the queue).
23
+ * pool: assume that, even when signalled that a connection is available, context may be switched to a session which also checks the same connection out, before it's able to pick it up; in such a case, start from the beginning, until the pool timeout expires.
24
+ * session: forego bookkeeping when a connection is coalesced (instead, allow it to be dropped).
25
+ * digest_auth: make sure that an array is sent back if the probe response fails.
26
+ * alt-svc: when alt-svc handshake happens with more in-flight requests, defer termination to when these requests are made.
27
+ * http2: fix use of unexisting var `ex` when processing the connection closed callback.
28
+ * connection: fix potential session dereferencing, which allowed connections to be used across sessions, therefore bypassing needed synchronization and leading to the `undefined method 'after' for nil:NilClass` error.
29
+ * selector: close only selected connections (instead of all selectable connections) when an error occurs during IO readiness wait calls.
30
+ * resolvers: correctly propagate abrupt termination errors to the connection objects waiting for the answer.
31
+ * resolvers: when errors happenm force-close unresolved connections (and ensure they're both pinned to the corresponding session before the error happens, and are unpinned after error is propagated).
32
+ * resolvers: ensure resolvers transition to "closed" state, on all cases, when any error happens.
33
+ * resolvers: ensure that the next hostname is resolved when a timeout happens on the current one.
34
+ * native resolver: fixed duplication of the hostname to resolve in the list of candidates.
35
+ * https resolver: use a `system` resolver to resolve the DoH server hostname (instead of rerouting it to itself).
36
+ * https resolver: skip loop error reporting when error happens outside of it.
37
+ * https resolver: close connection on resolve errors, which prevents it from being around in the pool after termination; also deactivate it after successful use.
38
+ * multi resolver: do not check resolvers back into the pool if it's a multi resolver and the peer is still resolving (and do the check outside of the critical area).
39
+ * sentry adapter: removed usage of deprecated method which has been removed in sentry-ruby 6.0.0.
40
+ * selector: when coalescing connections, pin the current session before merging connections, to prevent it from registering in a selector being used in a different thread, and inadvertedly allowing it to be used across threads.
41
+ * session: fix: always pin connection before early-or-lazy resolution (fixes connection pool accounting under connection coalescing).
42
+
43
+ ## Chores
44
+
45
+ * logging emits a timestamp as well (to monitor timeouts).
46
+ * `:stream_bidi` plugin: extends HTTP2 module by using plugin extensions.
47
+ * connection: remove session/selector references when closing a connection (prevents leaking them beyond the usage scope).
@@ -32,7 +32,7 @@ module HTTPX::Plugins
32
32
 
33
33
  return unless config.propagate_traces && config.trace_propagation_targets.any? { |target| url.match?(target) }
34
34
 
35
- trace = ::Sentry.get_current_client.generate_sentry_trace(sentry_span)
35
+ trace = sentry_span.to_sentry_trace
36
36
  request.headers[::Sentry::SENTRY_TRACE_HEADER_NAME] = trace if trace
37
37
  end
38
38
 
@@ -44,12 +44,12 @@ module HTTPX
44
44
  @max_requests = @options.max_requests || MAX_REQUESTS
45
45
  @parser.reset!
46
46
  @handshake_completed = false
47
- @pending.concat(@requests) unless @requests.empty?
47
+ @pending.unshift(*@requests)
48
48
  end
49
49
 
50
50
  def close
51
51
  reset
52
- emit(:close, true)
52
+ emit(:close)
53
53
  end
54
54
 
55
55
  def exhausted?
@@ -114,7 +114,7 @@ module HTTPX
114
114
  @parser.http_version.join("."),
115
115
  headers)
116
116
  log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version.join(".")}" }
117
- log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{log_redact(v)}" }.join("\n") }
117
+ log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v)}" }.join("\n") }
118
118
 
119
119
  @request.response = response
120
120
  on_complete if response.finished?
@@ -126,7 +126,7 @@ module HTTPX
126
126
  response = @request.response
127
127
  log(level: 2) { "trailer headers received" }
128
128
 
129
- log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{log_redact(v.join(", "))}" }.join("\n") }
129
+ log(color: :yellow) { h.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v.join(", "))}" }.join("\n") }
130
130
  response.merge_headers(h)
131
131
  end
132
132
 
@@ -136,7 +136,7 @@ module HTTPX
136
136
  return unless request
137
137
 
138
138
  log(color: :green) { "-> DATA: #{chunk.bytesize} bytes..." }
139
- log(level: 2, color: :green) { "-> #{log_redact(chunk.inspect)}" }
139
+ log(level: 2, color: :green) { "-> #{log_redact_body(chunk.inspect)}" }
140
140
  response = request.response
141
141
 
142
142
  response << chunk
@@ -182,7 +182,7 @@ module HTTPX
182
182
  end
183
183
 
184
184
  if exhausted?
185
- @pending.concat(@requests)
185
+ @pending.unshift(*@requests)
186
186
  @requests.clear
187
187
 
188
188
  emit(:exhausted)
@@ -236,7 +236,7 @@ module HTTPX
236
236
  when /keep-alive/i
237
237
  if @handshake_completed
238
238
  if @max_requests.zero?
239
- @pending.concat(@requests)
239
+ @pending.unshift(*@requests)
240
240
  @requests.clear
241
241
  emit(:exhausted)
242
242
  end
@@ -360,7 +360,7 @@ module HTTPX
360
360
 
361
361
  while (chunk = request.drain_body)
362
362
  log(color: :green) { "<- DATA: #{chunk.bytesize} bytes..." }
363
- log(level: 2, color: :green) { "<- #{log_redact(chunk.inspect)}" }
363
+ log(level: 2, color: :green) { "<- #{log_redact_body(chunk.inspect)}" }
364
364
  @buffer << chunk
365
365
  throw(:buffer_full, request) if @buffer.full?
366
366
  end
@@ -381,7 +381,7 @@ module HTTPX
381
381
  def join_headers2(headers)
382
382
  headers.each do |field, value|
383
383
  field = capitalized(field)
384
- log(color: :yellow) { "<- HEADER: #{[field, log_redact(value)].join(": ")}" }
384
+ log(color: :yellow) { "<- HEADER: #{[field, log_redact_headers(value)].join(": ")}" }
385
385
  @buffer << "#{field}: #{value}#{CRLF}"
386
386
  end
387
387
  end
@@ -89,7 +89,7 @@ module HTTPX
89
89
  @connection.goaway
90
90
  emit(:timeout, @options.timeout[:close_handshake_timeout])
91
91
  end
92
- emit(:close, true)
92
+ emit(:close)
93
93
  end
94
94
 
95
95
  def empty?
@@ -234,12 +234,12 @@ module HTTPX
234
234
  extra_headers = set_protocol_headers(request)
235
235
 
236
236
  if request.headers.key?("host")
237
- log { "forbidden \"host\" header found (#{log_redact(request.headers["host"])}), will use it as authority..." }
237
+ log { "forbidden \"host\" header found (#{log_redact_headers(request.headers["host"])}), will use it as authority..." }
238
238
  extra_headers[":authority"] = request.headers["host"]
239
239
  end
240
240
 
241
241
  log(level: 1, color: :yellow) do
242
- "\n#{request.headers.merge(extra_headers).each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{log_redact(v)}" }.join("\n")}"
242
+ "\n#{request.headers.merge(extra_headers).each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{log_redact_headers(v)}" }.join("\n")}"
243
243
  end
244
244
  stream.headers(request.headers.each(extra_headers), end_stream: request.body.empty?)
245
245
  end
@@ -251,7 +251,7 @@ module HTTPX
251
251
  end
252
252
 
253
253
  log(level: 1, color: :yellow) do
254
- request.trailers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{log_redact(v)}" }.join("\n")
254
+ request.trailers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{log_redact_headers(v)}" }.join("\n")
255
255
  end
256
256
  stream.headers(request.trailers.each, end_stream: true)
257
257
  end
@@ -279,7 +279,7 @@ module HTTPX
279
279
 
280
280
  def send_chunk(request, stream, chunk, next_chunk)
281
281
  log(level: 1, color: :green) { "#{stream.id}: -> DATA: #{chunk.bytesize} bytes..." }
282
- log(level: 2, color: :green) { "#{stream.id}: -> #{log_redact(chunk.inspect)}" }
282
+ log(level: 2, color: :green) { "#{stream.id}: -> #{log_redact_body(chunk.inspect)}" }
283
283
  stream.data(chunk, end_stream: end_stream?(request, next_chunk))
284
284
  end
285
285
 
@@ -300,7 +300,7 @@ module HTTPX
300
300
  end
301
301
 
302
302
  log(color: :yellow) do
303
- h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{log_redact(v)}" }.join("\n")
303
+ h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{log_redact_headers(v)}" }.join("\n")
304
304
  end
305
305
  _, status = h.shift
306
306
  headers = request.options.headers_class.new(h)
@@ -313,14 +313,14 @@ module HTTPX
313
313
 
314
314
  def on_stream_trailers(stream, response, h)
315
315
  log(color: :yellow) do
316
- h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{log_redact(v)}" }.join("\n")
316
+ h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{log_redact_headers(v)}" }.join("\n")
317
317
  end
318
318
  response.merge_headers(h)
319
319
  end
320
320
 
321
321
  def on_stream_data(stream, request, data)
322
322
  log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
323
- log(level: 2, color: :green) { "#{stream.id}: <- #{log_redact(data.inspect)}" }
323
+ log(level: 2, color: :green) { "#{stream.id}: <- #{log_redact_body(data.inspect)}" }
324
324
  request.response << data
325
325
  end
326
326
 
@@ -387,18 +387,17 @@ module HTTPX
387
387
  end
388
388
  else
389
389
  ex = GoawayError.new(error)
390
+ ex.set_backtrace(caller)
391
+
390
392
  @pending.unshift(*@streams.keys)
391
393
  teardown
392
- end
393
394
 
394
- if ex
395
- ex.set_backtrace(caller)
396
395
  handle_error(ex)
397
396
  end
398
397
  end
399
398
  return unless is_connection_closed && @streams.empty?
400
399
 
401
- emit(:close, is_connection_closed)
400
+ emit(:close) if is_connection_closed
402
401
  end
403
402
 
404
403
  def on_frame_sent(frame)
@@ -409,7 +408,7 @@ module HTTPX
409
408
  when :data
410
409
  frame.merge(payload: frame[:payload].bytesize)
411
410
  when :headers, :ping
412
- frame.merge(payload: log_redact(frame[:payload]))
411
+ frame.merge(payload: log_redact_headers(frame[:payload]))
413
412
  else
414
413
  frame
415
414
  end
@@ -425,7 +424,7 @@ module HTTPX
425
424
  when :data
426
425
  frame.merge(payload: frame[:payload].bytesize)
427
426
  when :headers, :ping
428
- frame.merge(payload: log_redact(frame[:payload]))
427
+ frame.merge(payload: log_redact_headers(frame[:payload]))
429
428
  else
430
429
  frame
431
430
  end
@@ -435,7 +434,7 @@ module HTTPX
435
434
 
436
435
  def on_altsvc(origin, frame)
437
436
  log(level: 2) { "#{frame[:stream]}: altsvc frame was received" }
438
- log(level: 2) { "#{frame[:stream]}: #{log_redact(frame.inspect)}" }
437
+ log(level: 2) { "#{frame[:stream]}: #{log_redact_headers(frame.inspect)}" }
439
438
  alt_origin = URI.parse("#{frame[:proto]}://#{frame[:host]}:#{frame[:port]}")
440
439
  params = { "ma" => frame[:max_age] }
441
440
  emit(:altsvc, origin, alt_origin, origin, params)
@@ -34,8 +34,6 @@ module HTTPX
34
34
 
35
35
  using URIExtensions
36
36
 
37
- def_delegator :@io, :closed?
38
-
39
37
  def_delegator :@write_buffer, :empty?
40
38
 
41
39
  attr_reader :type, :io, :origin, :origins, :state, :pending, :options, :ssl_session, :sibling
@@ -48,9 +46,9 @@ module HTTPX
48
46
 
49
47
  def initialize(uri, options)
50
48
  @current_session = @current_selector =
51
- @parser = @sibling = @coalesced_connection =
52
- @family = @io = @ssl_session = @timeout =
53
- @connected_at = @response_received_at = nil
49
+ @parser = @sibling = @coalesced_connection = @altsvc_connection =
50
+ @family = @io = @ssl_session = @timeout =
51
+ @connected_at = @response_received_at = nil
54
52
 
55
53
  @exhausted = @cloned = @main_sibling = false
56
54
 
@@ -65,7 +63,6 @@ module HTTPX
65
63
  @inflight = 0
66
64
  @keep_alive_timeout = @options.timeout[:keep_alive_timeout]
67
65
 
68
- on(:error, &method(:on_error))
69
66
  if @options.io
70
67
  # if there's an already open IO, get its
71
68
  # peer address, and force-initiate the parser
@@ -75,32 +72,6 @@ module HTTPX
75
72
  else
76
73
  transition(:idle)
77
74
  end
78
- on(:close) do
79
- next if @exhausted # it'll reset
80
-
81
- # may be called after ":close" above, so after the connection has been checked back in.
82
- # next unless @current_session
83
-
84
- next unless @current_session
85
-
86
- @current_session.deselect_connection(self, @current_selector, @cloned)
87
- end
88
- on(:terminate) do
89
- next if @exhausted # it'll reset
90
-
91
- current_session = @current_session
92
- current_selector = @current_selector
93
-
94
- # may be called after ":close" above, so after the connection has been checked back in.
95
- next unless current_session && current_selector
96
-
97
- current_session.deselect_connection(self, current_selector)
98
- end
99
-
100
- on(:altsvc) do |alt_origin, origin, alt_params|
101
- build_altsvc_connection(alt_origin, origin, alt_params)
102
- end
103
-
104
75
  self.addresses = @options.addresses if @options.addresses
105
76
  end
106
77
 
@@ -158,6 +129,10 @@ module HTTPX
158
129
  connection.merge(self)
159
130
  end
160
131
 
132
+ def coalesced?
133
+ @coalesced_connection
134
+ end
135
+
161
136
  # coalescable connections need to be mergeable!
162
137
  # but internally, #mergeable? is called before #coalescable?
163
138
  def coalescable?(connection)
@@ -231,7 +206,7 @@ module HTTPX
231
206
 
232
207
  nil
233
208
  rescue StandardError => e
234
- emit(:error, e)
209
+ on_error(e)
235
210
  nil
236
211
  end
237
212
 
@@ -259,7 +234,9 @@ module HTTPX
259
234
  nil
260
235
  rescue StandardError => e
261
236
  @write_buffer.clear
262
- emit(:error, e)
237
+ on_error(e)
238
+ rescue Exception => e # rubocop:disable Lint/RescueException
239
+ force_close(true)
263
240
  raise e
264
241
  end
265
242
 
@@ -273,7 +250,7 @@ module HTTPX
273
250
  case @state
274
251
  when :idle
275
252
  purge_after_closed
276
- emit(:terminate)
253
+ disconnect
277
254
  when :closed
278
255
  @connected_at = nil
279
256
  end
@@ -281,6 +258,23 @@ module HTTPX
281
258
  close
282
259
  end
283
260
 
261
+ # bypasses state machine rules while setting the connection in the
262
+ # :closed state.
263
+ def force_close(delete_pending = false)
264
+ if delete_pending
265
+ @pending.clear
266
+ elsif (parser = @parser)
267
+ enqueue_pending_requests_from_parser(parser)
268
+ end
269
+ return if @state == :closed
270
+
271
+ @state = :closed
272
+ @write_buffer.clear
273
+ purge_after_closed
274
+ disconnect
275
+ emit(:force_closed, delete_pending)
276
+ end
277
+
284
278
  # bypasses the state machine to force closing of connections still connecting.
285
279
  # **only** used for Happy Eyeballs v2.
286
280
  def force_reset(cloned = false)
@@ -368,18 +362,40 @@ module HTTPX
368
362
  end
369
363
 
370
364
  def handle_connect_error(error)
371
- return handle_error(error) unless @sibling && @sibling.connecting?
365
+ return on_error(error) unless @sibling && @sibling.connecting?
372
366
 
373
367
  @sibling.merge(self)
374
368
 
375
369
  force_reset(true)
376
370
  end
377
371
 
372
+ # disconnects from the current session it's attached to
378
373
  def disconnect
379
- return unless @current_session && @current_selector
374
+ return if @exhausted # it'll reset
375
+
376
+ return unless (current_session = @current_session) && (current_selector = @current_selector)
380
377
 
381
- emit(:close)
382
378
  @current_session = @current_selector = nil
379
+
380
+ current_session.deselect_connection(self, current_selector, @cloned)
381
+ end
382
+
383
+ def on_error(error, request = nil)
384
+ if error.is_a?(OperationTimeoutError)
385
+
386
+ # inactive connections do not contribute to the select loop, therefore
387
+ # they should not fail due to such errors.
388
+ return if @state == :inactive
389
+
390
+ if @timeout
391
+ @timeout -= error.timeout
392
+ return unless @timeout <= 0
393
+ end
394
+
395
+ error = error.to_connection_error if connecting?
396
+ end
397
+ handle_error(error, request)
398
+ reset
383
399
  end
384
400
 
385
401
  # :nocov:
@@ -417,7 +433,11 @@ module HTTPX
417
433
  # * the number of pending requests
418
434
  # * whether the write buffer has bytes (i.e. for close handshake)
419
435
  if @pending.empty? && @inflight.zero? && @write_buffer.empty?
420
- log(level: 3) { "NO MORE REQUESTS..." }
436
+ log(level: 3) { "NO MORE REQUESTS..." } if @parser && @parser.pending.any?
437
+
438
+ # terminate if an altsvc connection has been established
439
+ terminate if @altsvc_connection
440
+
421
441
  return
422
442
  end
423
443
 
@@ -462,7 +482,14 @@ module HTTPX
462
482
  break if @state == :closing || @state == :closed
463
483
 
464
484
  # exit #consume altogether if all outstanding requests have been dealt with
465
- return if @pending.empty? && @inflight.zero?
485
+ if @pending.empty? && @inflight.zero? && @write_buffer.empty? # rubocop:disable Style/Next
486
+ log(level: 3) { "NO MORE REQUESTS..." } if @parser && @parser.pending.any?
487
+
488
+ # terminate if an altsvc connection has been established
489
+ terminate if @altsvc_connection
490
+
491
+ return
492
+ end
466
493
  end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
467
494
 
468
495
  #
@@ -555,6 +582,17 @@ module HTTPX
555
582
  request.ping!
556
583
  end
557
584
 
585
+ def enqueue_pending_requests_from_parser(parser)
586
+ parser_pending_requests = parser.pending
587
+
588
+ return if parser_pending_requests.empty?
589
+
590
+ # the connection will be reused, so parser requests must come
591
+ # back to the pending list before the parser is reset.
592
+ @inflight -= parser_pending_requests.size
593
+ @pending.unshift(*parser_pending_requests)
594
+ end
595
+
558
596
  def build_parser(protocol = @io.protocol)
559
597
  parser = parser_type(protocol).new(@write_buffer, @options)
560
598
  set_parser_callbacks(parser)
@@ -564,7 +602,7 @@ module HTTPX
564
602
  def set_parser_callbacks(parser)
565
603
  parser.on(:response) do |request, response|
566
604
  AltSvc.emit(request, response) do |alt_origin, origin, alt_params|
567
- emit(:altsvc, alt_origin, origin, alt_params)
605
+ build_altsvc_connection(alt_origin, origin, alt_params)
568
606
  end
569
607
  @response_received_at = Utils.now
570
608
  @inflight -= 1
@@ -572,7 +610,7 @@ module HTTPX
572
610
  request.emit(:response, response)
573
611
  end
574
612
  parser.on(:altsvc) do |alt_origin, origin, alt_params|
575
- emit(:altsvc, alt_origin, origin, alt_params)
613
+ build_altsvc_connection(alt_origin, origin, alt_params)
576
614
  end
577
615
 
578
616
  parser.on(:pong, &method(:send_pending))
@@ -581,50 +619,31 @@ module HTTPX
581
619
  request.emit(:promise, parser, stream)
582
620
  end
583
621
  parser.on(:exhausted) do
622
+ enqueue_pending_requests_from_parser(parser)
623
+
584
624
  @exhausted = true
585
- current_session = @current_session
586
- current_selector = @current_selector
587
- begin
588
- parser.close
589
- @pending.concat(parser.pending)
590
- ensure
591
- @current_session = current_session
592
- @current_selector = current_selector
593
- end
625
+ parser.close
594
626
 
595
- case @state
596
- when :closed
597
- idling
598
- @exhausted = false
599
- when :closing
600
- once(:closed) do
601
- idling
602
- @exhausted = false
603
- end
604
- end
627
+ idling
628
+ @exhausted = false
605
629
  end
606
630
  parser.on(:origin) do |origin|
607
631
  @origins |= [origin]
608
632
  end
609
- parser.on(:close) do |force|
610
- if force
611
- reset
612
- emit(:terminate)
613
- end
633
+ parser.on(:close) do
634
+ reset
635
+ disconnect
614
636
  end
615
637
  parser.on(:close_handshake) do
616
- consume
638
+ consume unless @state == :closed
617
639
  end
618
640
  parser.on(:reset) do
619
- @pending.concat(parser.pending) unless parser.empty?
620
- current_session = @current_session
621
- current_selector = @current_selector
641
+ enqueue_pending_requests_from_parser(parser)
642
+
622
643
  reset
623
- unless @pending.empty?
624
- idling
625
- @current_session = current_session
626
- @current_selector = current_selector
627
- end
644
+ # :reset event only fired in http/1.1, so this guarantees
645
+ # that the connection will be closed here.
646
+ idling unless @pending.empty?
628
647
  end
629
648
  parser.on(:current_timeout) do
630
649
  @current_timeout = @timeout = parser.timeout
@@ -674,16 +693,12 @@ module HTTPX
674
693
  error = ConnectionError.new(e.message)
675
694
  error.set_backtrace(e.backtrace)
676
695
  handle_connect_error(error) if connecting?
677
- @state = :closed
678
- purge_after_closed
679
- disconnect
696
+ force_close
680
697
  rescue TLSError, ::HTTP2::Error::ProtocolError, ::HTTP2::Error::HandshakeError => e
681
698
  # connect errors, exit gracefully
682
699
  handle_error(e)
683
700
  handle_connect_error(e) if connecting?
684
- @state = :closed
685
- purge_after_closed
686
- disconnect
701
+ force_close
687
702
  end
688
703
 
689
704
  def handle_transition(nextstate)
@@ -711,6 +726,8 @@ module HTTPX
711
726
 
712
727
  # do not deactivate connection in use
713
728
  return if @inflight.positive? || @parser.waiting_for_ping?
729
+
730
+ disconnect
714
731
  when :closing
715
732
  return unless @state == :idle || @state == :open
716
733
 
@@ -785,6 +802,8 @@ module HTTPX
785
802
 
786
803
  # returns an HTTPX::Connection for the negotiated Alternative Service (or none).
787
804
  def build_altsvc_connection(alt_origin, origin, alt_params)
805
+ return if @altsvc_connection
806
+
788
807
  # do not allow security downgrades on altsvc negotiation
789
808
  return if @origin.scheme == "https" && alt_origin.scheme != "https"
790
809
 
@@ -802,10 +821,11 @@ module HTTPX
802
821
 
803
822
  connection.extend(AltSvc::ConnectionMixin) unless connection.is_a?(AltSvc::ConnectionMixin)
804
823
 
805
- log(level: 1) { "#{origin} alt-svc: #{alt_origin}" }
824
+ @altsvc_connection = connection
825
+
826
+ log(level: 1) { "#{origin}: alt-svc connection##{connection.object_id} established to #{alt_origin}" }
806
827
 
807
828
  connection.merge(self)
808
- terminate
809
829
  rescue UnsupportedSchemeError
810
830
  altsvc["noop"] = true
811
831
  nil
@@ -835,26 +855,8 @@ module HTTPX
835
855
  end
836
856
  end
837
857
 
838
- def on_error(error, request = nil)
839
- if error.is_a?(OperationTimeoutError)
840
-
841
- # inactive connections do not contribute to the select loop, therefore
842
- # they should not fail due to such errors.
843
- return if @state == :inactive
844
-
845
- if @timeout
846
- @timeout -= error.timeout
847
- return unless @timeout <= 0
848
- end
849
-
850
- error = error.to_connection_error if connecting?
851
- end
852
- handle_error(error, request)
853
- reset
854
- end
855
-
856
858
  def handle_error(error, request = nil)
857
- parser.handle_error(error, request) if @parser && parser.respond_to?(:handle_error)
859
+ parser.handle_error(error, request) if @parser && @parser.respond_to?(:handle_error)
858
860
  while (req = @pending.shift)
859
861
  next if request && req == request
860
862
 
@@ -929,6 +931,17 @@ module HTTPX
929
931
 
930
932
  def set_request_timeout(label, request, timeout, start_event, finish_events, &callback)
931
933
  request.set_timeout_callback(start_event) do
934
+ unless @current_selector
935
+ raise Error, "request has been resend to an out-of-session connection, and this " \
936
+ "should never happen!!! Please report this error! " \
937
+ "(state:#{@state}, " \
938
+ "parser?:#{!!@parser}, " \
939
+ "bytes in write buffer?:#{!@write_buffer.empty?}, " \
940
+ "cloned?:#{@cloned}, " \
941
+ "sibling?:#{!!@sibling}, " \
942
+ "coalesced?:#{coalesced?})"
943
+ end
944
+
932
945
  timer = @current_selector.after(timeout, callback)
933
946
  request.active_timeouts << label
934
947
 
@@ -4,20 +4,6 @@ require "uri"
4
4
 
5
5
  module HTTPX
6
6
  module ArrayExtensions
7
- module FilterMap
8
- refine Array do
9
- # Ruby 2.7 backport
10
- def filter_map
11
- return to_enum(:filter_map) unless block_given?
12
-
13
- each_with_object([]) do |item, res|
14
- processed = yield(item)
15
- res << processed if processed
16
- end
17
- end
18
- end unless Array.method_defined?(:filter_map)
19
- end
20
-
21
7
  module Intersect
22
8
  refine Array do
23
9
  # Ruby 3.1 backport
data/lib/httpx/io/ssl.rb CHANGED
@@ -98,7 +98,7 @@ module HTTPX
98
98
  end
99
99
 
100
100
  unless @io.is_a?(OpenSSL::SSL::SSLSocket)
101
- if (hostname_is_ip = (@ip == @sni_hostname))
101
+ if (hostname_is_ip = (@ip == @sni_hostname)) && @ctx.verify_hostname
102
102
  # IPv6 address would be "[::1]", must turn to "0000:0000:0000:0000:0000:0000:0000:0001" for cert SAN check
103
103
  @sni_hostname = @ip.to_string
104
104
  # IP addresses in SNI is not valid per RFC 6066, section 3.