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.
- checksums.yaml +4 -4
- data/doc/release_notes/1_8_1.md +1 -1
- data/doc/release_notes/1_8_2.md +4 -4
- data/doc/release_notes/1_8_3.md +3 -5
- data/doc/release_notes/1_8_4.md +1 -1
- data/doc/release_notes/1_9_0.md +75 -0
- data/doc/release_notes/1_9_1.md +32 -0
- data/lib/httpx/adapters/faraday.rb +1 -1
- data/lib/httpx/adapters/webmock.rb +15 -2
- data/lib/httpx/altsvc.rb +29 -15
- data/lib/httpx/chainable.rb +4 -1
- data/lib/httpx/connection/http1.rb +34 -35
- data/lib/httpx/connection/http2.rb +146 -33
- data/lib/httpx/connection.rb +282 -162
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/tcp.rb +42 -21
- data/lib/httpx/options.rb +86 -35
- data/lib/httpx/parser/http1.rb +3 -6
- data/lib/httpx/plugins/auth/digest.rb +1 -2
- data/lib/httpx/plugins/auth.rb +11 -2
- data/lib/httpx/plugins/aws_sdk_authentication.rb +7 -3
- data/lib/httpx/plugins/aws_sigv4.rb +1 -1
- data/lib/httpx/plugins/basic_auth.rb +2 -0
- data/lib/httpx/plugins/cache.rb +1 -1
- data/lib/httpx/plugins/callbacks.rb +8 -6
- data/lib/httpx/plugins/circuit_breaker.rb +1 -0
- data/lib/httpx/plugins/content_digest.rb +3 -5
- data/lib/httpx/plugins/digest_auth.rb +2 -0
- data/lib/httpx/plugins/expect.rb +10 -1
- data/lib/httpx/plugins/fiber_concurrency.rb +59 -16
- data/lib/httpx/plugins/follow_redirects.rb +1 -1
- data/lib/httpx/plugins/grpc.rb +21 -0
- data/lib/httpx/plugins/h2c.rb +12 -11
- data/lib/httpx/plugins/hedging.rb +284 -0
- data/lib/httpx/plugins/ntlm_auth.rb +5 -3
- data/lib/httpx/plugins/ntlm_v2_auth.rb +5 -3
- data/lib/httpx/plugins/oauth.rb +2 -2
- data/lib/httpx/plugins/proxy/http.rb +67 -26
- data/lib/httpx/plugins/proxy/socks5.rb +4 -4
- data/lib/httpx/plugins/proxy/ssh.rb +30 -11
- data/lib/httpx/plugins/proxy.rb +68 -54
- data/lib/httpx/plugins/push_promise.rb +4 -2
- data/lib/httpx/plugins/query.rb +1 -0
- data/lib/httpx/plugins/retries.rb +2 -2
- data/lib/httpx/plugins/server_sent_events.rb +1 -0
- data/lib/httpx/plugins/ssrf_filter.rb +1 -1
- data/lib/httpx/plugins/stream.rb +12 -10
- data/lib/httpx/plugins/stream_bidi.rb +4 -0
- data/lib/httpx/plugins/tracing.rb +99 -11
- data/lib/httpx/plugins/upgrade/h2.rb +3 -3
- data/lib/httpx/plugins/upgrade.rb +2 -0
- data/lib/httpx/plugins/webdav.rb +8 -0
- data/lib/httpx/request/body.rb +2 -1
- data/lib/httpx/request.rb +6 -6
- data/lib/httpx/resolver/https.rb +3 -2
- data/lib/httpx/resolver/native.rb +19 -9
- data/lib/httpx/resolver/resolver.rb +2 -2
- data/lib/httpx/resolver/system.rb +4 -0
- data/lib/httpx/resolver.rb +10 -6
- data/lib/httpx/response.rb +19 -7
- data/lib/httpx/selector.rb +111 -10
- data/lib/httpx/session.rb +36 -10
- data/lib/httpx/transcoder/json.rb +1 -1
- data/lib/httpx/version.rb +1 -1
- data/lib/httpx.rb +1 -0
- data/sig/altsvc.rbs +3 -3
- data/sig/callbacks.rbs +1 -0
- data/sig/chainable.rbs +3 -1
- data/sig/connection/http1.rbs +4 -2
- data/sig/connection/http2.rbs +23 -17
- data/sig/connection.rbs +29 -7
- data/sig/headers.rbs +1 -1
- data/sig/httpx.rbs +2 -0
- data/sig/io/tcp.rbs +5 -0
- data/sig/options.rbs +43 -12
- data/sig/parser/http1.rbs +1 -1
- data/sig/plugins/auth/digest.rbs +0 -1
- data/sig/plugins/auth.rbs +12 -5
- data/sig/plugins/expect.rbs +6 -0
- data/sig/plugins/fiber_concurrency.rbs +10 -1
- data/sig/plugins/hedging.rbs +44 -0
- data/sig/plugins/proxy/http.rbs +9 -2
- data/sig/plugins/proxy.rbs +3 -2
- data/sig/plugins/retries.rbs +2 -2
- data/sig/plugins/stream_bidi.rbs +0 -2
- data/sig/plugins/tracing.rbs +22 -5
- data/sig/request/body.rbs +1 -1
- data/sig/resolver/entry.rbs +2 -2
- data/sig/resolver/https.rbs +2 -0
- data/sig/resolver/native.rbs +3 -0
- data/sig/resolver/resolver.rbs +1 -1
- data/sig/resolver/system.rbs +2 -0
- data/sig/resolver.rbs +1 -1
- data/sig/response.rbs +1 -2
- data/sig/selector.rbs +17 -3
- data/sig/session.rbs +8 -6
- metadata +7 -1
data/lib/httpx/connection.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
236
|
+
try_connect
|
|
228
237
|
|
|
229
238
|
return @io.interests if connecting?
|
|
230
239
|
end
|
|
231
240
|
|
|
232
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 == :
|
|
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
|
-
|
|
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
|
-
|
|
425
|
+
timeout = @timeout || @options.timeout.operation_timeout
|
|
401
426
|
|
|
402
|
-
return
|
|
427
|
+
return timeout unless connecting? && (attempt_timeout = connect_attempt_time_left)
|
|
403
428
|
|
|
404
|
-
|
|
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
|
-
|
|
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
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
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
|
|
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? &&
|
|
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 == :
|
|
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
|
|
667
|
-
|
|
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
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(:
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
899
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1046
|
-
|
|
1047
|
-
@no_more_requests_counter += 1
|
|
1117
|
+
def no_more_selector_check
|
|
1118
|
+
return if @current_selector
|
|
1048
1119
|
|
|
1049
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
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? &&
|
|
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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
next if request && req == request
|
|
1158
|
+
else
|
|
1159
|
+
while (req = @pending.shift)
|
|
1160
|
+
next if request && req == request
|
|
1083
1161
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
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
|
-
|
|
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)
|