httpx 1.8.4 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_8_1.md +1 -1
  3. data/doc/release_notes/1_8_2.md +4 -4
  4. data/doc/release_notes/1_8_3.md +3 -5
  5. data/doc/release_notes/1_8_4.md +1 -1
  6. data/doc/release_notes/1_9_0.md +75 -0
  7. data/doc/release_notes/1_9_1.md +32 -0
  8. data/lib/httpx/adapters/faraday.rb +1 -1
  9. data/lib/httpx/adapters/webmock.rb +15 -2
  10. data/lib/httpx/altsvc.rb +29 -15
  11. data/lib/httpx/chainable.rb +4 -1
  12. data/lib/httpx/connection/http1.rb +34 -35
  13. data/lib/httpx/connection/http2.rb +146 -33
  14. data/lib/httpx/connection.rb +282 -162
  15. data/lib/httpx/headers.rb +2 -2
  16. data/lib/httpx/io/tcp.rb +42 -21
  17. data/lib/httpx/options.rb +86 -35
  18. data/lib/httpx/parser/http1.rb +3 -6
  19. data/lib/httpx/plugins/auth/digest.rb +1 -2
  20. data/lib/httpx/plugins/auth.rb +11 -2
  21. data/lib/httpx/plugins/aws_sdk_authentication.rb +7 -3
  22. data/lib/httpx/plugins/aws_sigv4.rb +1 -1
  23. data/lib/httpx/plugins/basic_auth.rb +2 -0
  24. data/lib/httpx/plugins/cache.rb +1 -1
  25. data/lib/httpx/plugins/callbacks.rb +8 -6
  26. data/lib/httpx/plugins/circuit_breaker.rb +1 -0
  27. data/lib/httpx/plugins/content_digest.rb +3 -5
  28. data/lib/httpx/plugins/digest_auth.rb +2 -0
  29. data/lib/httpx/plugins/expect.rb +10 -1
  30. data/lib/httpx/plugins/fiber_concurrency.rb +59 -16
  31. data/lib/httpx/plugins/follow_redirects.rb +1 -1
  32. data/lib/httpx/plugins/grpc.rb +21 -0
  33. data/lib/httpx/plugins/h2c.rb +12 -11
  34. data/lib/httpx/plugins/hedging.rb +284 -0
  35. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  36. data/lib/httpx/plugins/ntlm_v2_auth.rb +5 -3
  37. data/lib/httpx/plugins/oauth.rb +2 -2
  38. data/lib/httpx/plugins/proxy/http.rb +67 -26
  39. data/lib/httpx/plugins/proxy/socks5.rb +4 -4
  40. data/lib/httpx/plugins/proxy/ssh.rb +30 -11
  41. data/lib/httpx/plugins/proxy.rb +68 -54
  42. data/lib/httpx/plugins/push_promise.rb +4 -2
  43. data/lib/httpx/plugins/query.rb +1 -0
  44. data/lib/httpx/plugins/retries.rb +2 -2
  45. data/lib/httpx/plugins/server_sent_events.rb +1 -0
  46. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  47. data/lib/httpx/plugins/stream.rb +12 -10
  48. data/lib/httpx/plugins/stream_bidi.rb +4 -0
  49. data/lib/httpx/plugins/tracing.rb +99 -11
  50. data/lib/httpx/plugins/upgrade/h2.rb +3 -3
  51. data/lib/httpx/plugins/upgrade.rb +2 -0
  52. data/lib/httpx/plugins/webdav.rb +8 -0
  53. data/lib/httpx/request/body.rb +2 -1
  54. data/lib/httpx/request.rb +6 -6
  55. data/lib/httpx/resolver/https.rb +3 -2
  56. data/lib/httpx/resolver/native.rb +19 -9
  57. data/lib/httpx/resolver/resolver.rb +2 -2
  58. data/lib/httpx/resolver/system.rb +4 -0
  59. data/lib/httpx/resolver.rb +10 -6
  60. data/lib/httpx/response.rb +19 -7
  61. data/lib/httpx/selector.rb +111 -10
  62. data/lib/httpx/session.rb +36 -10
  63. data/lib/httpx/transcoder/json.rb +1 -1
  64. data/lib/httpx/version.rb +1 -1
  65. data/lib/httpx.rb +1 -0
  66. data/sig/altsvc.rbs +3 -3
  67. data/sig/callbacks.rbs +1 -0
  68. data/sig/chainable.rbs +3 -1
  69. data/sig/connection/http1.rbs +4 -2
  70. data/sig/connection/http2.rbs +23 -17
  71. data/sig/connection.rbs +29 -7
  72. data/sig/headers.rbs +1 -1
  73. data/sig/httpx.rbs +2 -0
  74. data/sig/io/tcp.rbs +5 -0
  75. data/sig/options.rbs +43 -12
  76. data/sig/parser/http1.rbs +1 -1
  77. data/sig/plugins/auth/digest.rbs +0 -1
  78. data/sig/plugins/auth.rbs +12 -5
  79. data/sig/plugins/expect.rbs +6 -0
  80. data/sig/plugins/fiber_concurrency.rbs +10 -1
  81. data/sig/plugins/hedging.rbs +44 -0
  82. data/sig/plugins/proxy/http.rbs +9 -2
  83. data/sig/plugins/proxy.rbs +3 -2
  84. data/sig/plugins/retries.rbs +2 -2
  85. data/sig/plugins/stream_bidi.rbs +0 -2
  86. data/sig/plugins/tracing.rbs +22 -5
  87. data/sig/request/body.rbs +1 -1
  88. data/sig/resolver/entry.rbs +2 -2
  89. data/sig/resolver/https.rbs +2 -0
  90. data/sig/resolver/native.rbs +3 -0
  91. data/sig/resolver/resolver.rbs +1 -1
  92. data/sig/resolver/system.rbs +2 -0
  93. data/sig/resolver.rbs +1 -1
  94. data/sig/response.rbs +1 -2
  95. data/sig/selector.rbs +17 -3
  96. data/sig/session.rbs +8 -6
  97. metadata +7 -1
@@ -50,11 +50,11 @@ module HTTPX
50
50
 
51
51
  attr_reader :streams, :pending
52
52
 
53
- def initialize(buffer, options)
54
- @callbacks = nil
53
+ def initialize(buffer, pending, options)
54
+ @callbacks = @last_error = nil
55
55
  @options = options
56
56
  @settings = @options.http2_settings
57
- @pending = []
57
+ @pending = pending
58
58
  @streams = {}
59
59
  @drains = {}
60
60
  @pings = []
@@ -68,16 +68,21 @@ module HTTPX
68
68
  end
69
69
 
70
70
  def timeout
71
- return @options.timeout[:operation_timeout] if @handshake_completed
71
+ return @options.timeout.operation_timeout if @handshake_completed
72
72
 
73
- @options.timeout[:settings_timeout]
73
+ @options.timeout.settings_timeout
74
74
  end
75
75
 
76
76
  def interests
77
- if @connection.closed?
77
+ if @connection.state == :closed
78
78
  return unless @handshake_completed
79
79
 
80
- return if @buffer.empty?
80
+ if @buffer.empty?
81
+ # a closed HTTP/2 connection can still receive DATA frames from inflight streams.
82
+ return :r unless @streams.empty?
83
+
84
+ return
85
+ end
81
86
 
82
87
  # HTTP/2 GOAWAY frame buffered.
83
88
  return :w
@@ -85,7 +90,7 @@ module HTTPX
85
90
 
86
91
  unless @connection.state == :connected && @handshake_completed
87
92
  # HTTP/2 in intermediate state or still completing initialization-
88
- return @buffer.empty? ? :r : :rw
93
+ return @buffer.empty? ? :r : :w
89
94
  end
90
95
 
91
96
  unless @connection.send_buffer.empty?
@@ -98,26 +103,69 @@ module HTTPX
98
103
  end
99
104
 
100
105
  # there are pending bufferable requests
101
- return :w if !@pending.empty? && can_buffer_more_requests?
106
+ if !@pending.empty? && can_buffer_pending_requests?
107
+ # only wait for writable if not waiting on the peer.
108
+ return waiting_for_peer? ? :rw : :w
109
+ end
110
+
111
+ unless @drains.empty?
112
+ return :w unless @buffer.empty?
102
113
 
103
- # there are pending frames from the last run
104
- return :w unless @drains.empty?
114
+ streams_with_no_window = @drains.none? do |stream, _|
115
+ # if an existing stream has buffered bytes, it must wait for
116
+ # WINDOW_UPDATE frames
117
+ stream.send_buffer.empty?
118
+ end
119
+
120
+ return streams_with_no_window ? :r : :rw
121
+
122
+ end
105
123
 
106
124
  if @buffer.empty?
107
125
  # skip if no more requests or pings to process
108
- return if @streams.empty? && @pings.empty?
126
+ return unless waiting_for_peer?
109
127
 
110
128
  :r
111
129
  else
112
130
  # buffered frames
113
- :w
131
+ waiting_for_peer? ? :rw : :w
114
132
  end
115
133
  end
116
134
 
117
135
  def close
118
- unless @connection.closed?
136
+ if @connection.closed?
137
+ # if there are inflight streams waiting to be completed AFTER the peer alread shut down the
138
+ # connection from their end, raise-or-retry depending on whether the error is recoverable.
139
+ if (ex = @last_error)
140
+ if @streams.any? || @pending.any?
141
+ if ex.is_a?(GoawayError) && !ex.unrecoverable?
142
+ inflight_unprocessed_requests = @streams.map do |req, _|
143
+ req.transition(:idle)
144
+ req
145
+ end
146
+ @streams.clear
147
+
148
+ # only signal exhaustion when inflight requests had to be moved back.
149
+ unless inflight_unprocessed_requests.empty?
150
+ @pending.unshift(*inflight_unprocessed_requests)
151
+ emit(:exhausted, ex)
152
+ end
153
+ else
154
+ while (req, _ = @streams.shift)
155
+ emit(:error, req, ex)
156
+ end
157
+ while (req = @pending.shift)
158
+ emit(:error, req, ex)
159
+ end
160
+ end
161
+ end
162
+
163
+ # clean pending payload now that everything's gone.
164
+ @buffer.clear
165
+ end
166
+ else
119
167
  @connection.goaway
120
- emit(:timeout, @options.timeout[:close_handshake_timeout])
168
+ emit(:timeout, @options.timeout.close_handshake_timeout)
121
169
  end
122
170
  emit(:close)
123
171
  end
@@ -138,10 +186,16 @@ module HTTPX
138
186
  # data received by the peer.
139
187
  emit_stream_error(stream, request, error)
140
188
  end
189
+
190
+ # if the connection transitioned to :closed, start tearing it down. Explicitly bypass the
191
+ # :closing transition to give it time to complete the inflight streams.
192
+ return unless @connection.state == :closed
193
+
194
+ close
141
195
  end
142
196
 
143
197
  def send(request, head = false)
144
- unless can_buffer_more_requests?
198
+ unless can_buffer_pending_requests?
145
199
  head ? @pending.unshift(request) : @pending << request
146
200
  return false
147
201
  end
@@ -150,6 +204,7 @@ module HTTPX
150
204
  handle_stream(stream, request)
151
205
  @streams[request] = stream
152
206
  @max_requests -= 1
207
+ emit(:request_accepted, request)
153
208
  end
154
209
  handle(request, stream)
155
210
  true
@@ -158,6 +213,7 @@ module HTTPX
158
213
  false
159
214
  rescue ::HTTP2::Error::Error, ArgumentError => e
160
215
  emit(:error, request, e)
216
+ false
161
217
  end
162
218
 
163
219
  def consume
@@ -166,6 +222,8 @@ module HTTPX
166
222
 
167
223
  handle(request, stream)
168
224
  end
225
+
226
+ send_pending
169
227
  end
170
228
 
171
229
  def handle_error(ex, request = nil)
@@ -173,35 +231,60 @@ module HTTPX
173
231
  case ex
174
232
  when OperationTimeoutError
175
233
  if !@handshake_completed && @connection.state != :closed
234
+ # timed out waiting for HTTP/2 connection handshake to complete.
176
235
  @connection.goaway(:settings_timeout, "closing due to settings timeout")
177
236
  emit(:close_handshake)
178
237
  settings_ex = SettingsTimeoutError.new(ex.timeout, ex.message)
179
238
  settings_ex.set_backtrace(ex.backtrace)
180
239
  ex = settings_ex
240
+ elsif @connection.state == :closing
241
+ # failed to receive data for inflight streams in time.
242
+ ex = @last_error || ex
243
+ end
244
+ when EOFError
245
+ if @connection.state == :closing
246
+ # socket closed while waiting for inflight streams to finish
247
+ ex = @last_error || ex
181
248
  end
182
249
  when GoawayError
183
250
  last_stream_id = ex.last_stream_id
184
251
  end
185
252
 
186
253
  inflight_unprocessed_requests = [] #: Array[Request]
254
+ unprocessed_streams = [] #: Array[::HTTP2::Stream]
187
255
 
188
- while (req, stream = @streams.shift)
189
- next if request && request == req
256
+ @streams.delete_if do |req, stream|
257
+ # allow inflight streams to finish
258
+ next(false) if !last_stream_id.infinite? && stream.id <= last_stream_id
259
+
260
+ @drains.delete(stream)
261
+
262
+ next(true) if request && request == req
190
263
 
191
264
  if stream.id > last_stream_id
192
265
  req.transition(:idle)
193
266
  # unprocessed request
194
267
  inflight_unprocessed_requests << req
268
+ unprocessed_streams << stream
195
269
 
196
- next
270
+ next(true)
197
271
  end
198
272
 
199
273
  emit(:error, req, ex)
274
+ true
275
+ end
276
+
277
+ @pending.unshift(*inflight_unprocessed_requests) if inflight_unprocessed_requests.any?
278
+
279
+ unless @streams.empty?
280
+ unprocessed_streams.each(&:close)
281
+
282
+ # streams covered by the GOAWAY are still being processed.
283
+ return
200
284
  end
201
285
 
202
286
  if ex.is_a?(GoawayError) && !ex.unrecoverable?
203
287
  # resend unprocessed requests on a different connection
204
- @pending.unshift(*inflight_unprocessed_requests) if inflight_unprocessed_requests.any?
205
288
  emit(:exhausted, ex) if @pending.any?
206
289
  return
207
290
  end
@@ -224,18 +307,29 @@ module HTTPX
224
307
  @pings.any?
225
308
  end
226
309
 
310
+ def no_more_requests?
311
+ @streams.empty?
312
+ end
313
+
227
314
  def reset_requests; end
228
315
 
229
316
  private
230
317
 
231
- def can_buffer_more_requests?
232
- (@handshake_completed || !@wait_for_handshake) &&
318
+ # whether there are PING acks, or streams waiting for more data
319
+ # (that can be a response or WINDOW_UPDATE frames).
320
+ def waiting_for_peer?
321
+ waiting_for_ping? || @streams.any? { |r, s| !r.can_buffer? || !s.send_buffer.empty? }
322
+ end
323
+
324
+ def can_buffer_pending_requests?
325
+ !@connection.closed? &&
326
+ (@handshake_completed || !@wait_for_handshake) &&
233
327
  @streams.size < @max_concurrent_requests &&
234
328
  @streams.size < @max_requests
235
329
  end
236
330
 
237
331
  def send_pending
238
- while (request = @pending.shift)
332
+ while !@buffer.full? && can_buffer_pending_requests? && (request = @pending.shift)
239
333
  break unless send(request, true)
240
334
  end
241
335
  end
@@ -322,13 +416,23 @@ module HTTPX
322
416
  def join_body(stream, request)
323
417
  return if request.body.empty?
324
418
 
325
- chunk = @drains.delete(request) || request.drain_body
419
+ # the next chunk waits until the peer grants more window.
420
+ throw(:buffer_full) if @drains.key?(stream) && (!stream.send_buffer.empty? || !@connection.send_buffer.empty?)
421
+
422
+ chunk = @drains.delete(stream) || request.drain_body
326
423
  while chunk
327
424
  next_chunk = request.drain_body
328
425
  send_chunk(request, stream, chunk, next_chunk)
329
426
 
330
- if next_chunk && (@buffer.full? || request.body.unbounded_body?)
331
- @drains[request] = next_chunk
427
+ if next_chunk && (
428
+ # send window is full
429
+ @buffer.full? ||
430
+ # stream / chunked request, must write a chunk at a time
431
+ request.body.unbounded_body? ||
432
+ # peer window is full, must wait for WINDOW_UPDATE frame(s)
433
+ !stream.send_buffer.empty? || !@connection.send_buffer.empty?
434
+ )
435
+ @drains[stream] = next_chunk
332
436
  throw(:buffer_full)
333
437
  end
334
438
 
@@ -462,7 +566,10 @@ module HTTPX
462
566
  end
463
567
  end
464
568
  end
465
- send(@pending.shift) unless @pending.empty?
569
+
570
+ consume
571
+
572
+ send(@pending.shift, true) unless @pending.empty?
466
573
 
467
574
  return unless @streams.empty? && exhausted?
468
575
 
@@ -494,16 +601,21 @@ module HTTPX
494
601
  emit(:error, request, error)
495
602
  end
496
603
  else
497
- ex = GoawayError.new(error, last_stream_id)
604
+ # keep goaway error around, in case inflight streams never complete.
605
+ @last_error = ex = GoawayError.new(error, last_stream_id)
498
606
  ex.set_backtrace(caller)
499
607
 
500
608
  handle_error(ex)
501
- teardown
502
609
  end
503
610
  end
504
- return unless is_connection_closed && @streams.empty?
611
+ return unless is_connection_closed
505
612
 
506
- emit(:close) if is_connection_closed
613
+ if @streams.empty?
614
+ emit(:close)
615
+ else
616
+ # this connection should not start new streams.
617
+ emit(:closing)
618
+ end
507
619
  end
508
620
 
509
621
  def on_frame_sent(frame)
@@ -590,8 +702,9 @@ module HTTPX
590
702
 
591
703
  def teardown(request = nil)
592
704
  if request
593
- @drains.delete(request)
594
- @streams.delete(request)
705
+ if (stream = @streams.delete(request))
706
+ @drains.delete(stream)
707
+ end
595
708
  else
596
709
  @drains.clear
597
710
  @streams.clear