httpx 1.9.0 → 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 (61) 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 +7 -7
  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 +14 -1
  10. data/lib/httpx/connection/http1.rb +24 -17
  11. data/lib/httpx/connection/http2.rb +108 -29
  12. data/lib/httpx/connection.rb +158 -162
  13. data/lib/httpx/io/tcp.rb +24 -7
  14. data/lib/httpx/plugins/aws_sigv4.rb +0 -1
  15. data/lib/httpx/plugins/cache.rb +1 -1
  16. data/lib/httpx/plugins/callbacks.rb +1 -0
  17. data/lib/httpx/plugins/content_digest.rb +3 -5
  18. data/lib/httpx/plugins/expect.rb +8 -1
  19. data/lib/httpx/plugins/fiber_concurrency.rb +56 -19
  20. data/lib/httpx/plugins/h2c.rb +12 -11
  21. data/lib/httpx/plugins/hedging.rb +2 -2
  22. data/lib/httpx/plugins/ntlm_auth.rb +4 -3
  23. data/lib/httpx/plugins/ntlm_v2_auth.rb +4 -3
  24. data/lib/httpx/plugins/oauth.rb +1 -1
  25. data/lib/httpx/plugins/proxy/http.rb +23 -16
  26. data/lib/httpx/plugins/proxy/socks5.rb +4 -4
  27. data/lib/httpx/plugins/proxy.rb +8 -15
  28. data/lib/httpx/plugins/query.rb +1 -0
  29. data/lib/httpx/plugins/stream.rb +12 -10
  30. data/lib/httpx/plugins/stream_bidi.rb +4 -0
  31. data/lib/httpx/plugins/tracing.rb +62 -21
  32. data/lib/httpx/plugins/upgrade/h2.rb +2 -2
  33. data/lib/httpx/request.rb +1 -1
  34. data/lib/httpx/resolver/https.rb +3 -2
  35. data/lib/httpx/resolver/native.rb +19 -9
  36. data/lib/httpx/resolver/resolver.rb +2 -2
  37. data/lib/httpx/resolver/system.rb +4 -0
  38. data/lib/httpx/resolver.rb +5 -5
  39. data/lib/httpx/response.rb +5 -1
  40. data/lib/httpx/selector.rb +108 -7
  41. data/lib/httpx/transcoder/json.rb +1 -1
  42. data/lib/httpx/version.rb +1 -1
  43. data/lib/httpx.rb +1 -0
  44. data/sig/chainable.rbs +2 -1
  45. data/sig/connection/http1.rbs +4 -2
  46. data/sig/connection/http2.rbs +8 -2
  47. data/sig/connection.rbs +11 -6
  48. data/sig/io/tcp.rbs +5 -0
  49. data/sig/plugins/expect.rbs +6 -0
  50. data/sig/plugins/fiber_concurrency.rbs +10 -1
  51. data/sig/plugins/proxy/http.rbs +3 -1
  52. data/sig/plugins/stream_bidi.rbs +0 -2
  53. data/sig/plugins/tracing.rbs +10 -2
  54. data/sig/resolver/https.rbs +2 -0
  55. data/sig/resolver/native.rbs +3 -0
  56. data/sig/resolver/resolver.rbs +1 -1
  57. data/sig/resolver/system.rbs +2 -0
  58. data/sig/resolver.rbs +1 -1
  59. data/sig/selector.rbs +15 -1
  60. data/sig/session.rbs +1 -1
  61. metadata +3 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e272ec707fd07e29993d6f5eff50f84f40ad4da61503552f1de2698a8b56b72b
4
- data.tar.gz: 10ebe441bd62cc1fec49c3caedb7a564dd998d2dd49d0740b3224f583f23e506
3
+ metadata.gz: 8eaea3d273dcba77ecae2abaf1979fa7ebbfddadf5c8f67b71636aac594b12e6
4
+ data.tar.gz: d4e50760994c7dff6d7ae963427f721ce349add9455055e01fc7074b2a9350ed
5
5
  SHA512:
6
- metadata.gz: 860445bf67dc3bb69f9724d1dcc13f5a7c21a936420addd0923aa4d955be929afd95b87fb032f5a226d245d65b4e07654b6df431992e755341583a5f0e5c26f9
7
- data.tar.gz: 4f11cd526c5099fa92d47fbd6b08a4366af6a01f4075f3df96f861cf7f3ce33e17a34e69f436b453e51d8afa07535b2bc62c04e1c1f260942782b0f432cc7383
6
+ metadata.gz: ae04971cb261d34dd72b1cd724e66e57321909e087b122357ab4506dfb4955603fc3c2ea54c9e34e9e9dcf5484aa347c89ad8c72a9fe94db26706c6850fc5442
7
+ data.tar.gz: a1e5ed666d7419b80da97449150ee67c95d2b1b9855e77060fff123aeaeaf268fb4caafde3242c9421d9cd042e5df14a088ecad30c837449a9fdee2b3376f6cb
@@ -14,7 +14,7 @@
14
14
  ## Bugfixes
15
15
 
16
16
  * `:auth` plugin: handle edge case where `:auth_header_expires_at` returns no value.
17
- * `:digest_auth` plugin: do not retry if there was an issue building building the authorization header after the probe.
17
+ * `:digest_auth` plugin: do not retry if there was an issue building the authorization header after the probe.
18
18
  * `webmock` adapter: prevent mocked requests from opening a connection to the peer server that will not be used.
19
19
  * clear and cancel request timers on completion or reset, ensuring that they won't be retained way past the point where the request is gone.
20
20
  * connection: cancel and discard ping timer before closing (removes timer reference)
@@ -11,7 +11,7 @@ session = HTTPX.with(unknown_option: 1) # will raise a HTTPX::Error: unknown opt
11
11
 
12
12
  * Use `HTTP2::Connection#closed?` internally (instead of `#state`) to check for connection state; this will, in a future release of `http-2`, seamlessly support the new `:closing` state.
13
13
  * Add missing `response` attribute reader to `HTTPX::RequestTimeoutError`.
14
- * Improve correctness of calling `#dup` on requests and connections.
14
+ * Improve the correctness of calling `#dup` on requests and connections.
15
15
 
16
16
  ## Bugfixes
17
17
 
@@ -21,10 +21,10 @@ session = HTTPX.with(unknown_option: 1) # will raise a HTTPX::Error: unknown opt
21
21
  * `:retries` plugin: do not retry on any HTTP/2 frame error (only on GOAWAY or PING frame errors).
22
22
  * `:persistent` plugin: do not decrement request remaining retries on ping or GOAWAY frame errors.
23
23
  * `:tracing` plugin: set connection initialization time to nil no termination (so that it doesn't get reused on persistent connections after reuse).
24
- * `:follow_redirects` plugin: fixed issue when a timeout error would be raised but only bubble up to the root request, not properly closing the in-flight follow up request, and leaving the request handling phase in a broken state.
25
- * datadog adapter: the connection initialization time is now set only on connect or first request received, the reason, making it more accurate.
24
+ * `:follow_redirects` plugin: fixed issue when a timeout error would be raised but only bubbles up to the root request, not properly closing the in-flight follow up request, and leaving the request handling phase in a broken state.
25
+ * datadog adapter: the connection initialization time is now set only on connect or first request received, the reason being, making it more accurate.
26
26
  * webmock adapter: `.to_timeout` helper will make the request raise a `HTTPX::RequestTimeoutError` instead of a plain `TimeoutError` (otherwise, if using persistent sessions, it'd trigger a retry instead and mess with webmock request count bookkeeping).
27
27
  * native resolver: do not transition to `:open` if the socket was closed during resolution (which happens when an error is raised while the query bytes are buffered).
28
28
  * fix for when setting a new session callback on an ssl context object which has been frozen after the first connection attempt (done by binding the ssl context callback to an internal ivar which can be changed).
29
29
  * connection: protect against `Errno::ETIMEDOUT` errors on socket close by forcing transition to closed.
30
- * do not set a ping timer for HTTP/1.1 on keep alive flow, as there's no ping and the connection is reestablished.
30
+ * do not set a ping timer for HTTP/1.1 on keep-alive flow, as there's no ping and the connection is reestablished.
@@ -2,8 +2,6 @@
2
2
 
3
3
  ## Bugfixes
4
4
 
5
- options print a warning instead of raising an error on unknown option
6
-
7
- the approach introduced in the previous version, of erroring on something people have been accidentally relying on, is too strict in hindsight for a patch version. This was reverted, and instead, a deprecation warning will be printed, to give time for folks to migrate.
8
-
9
- the format of options messages (exceptions and warnings) also slightly changes to wrap the option in quotes, for proper highlighting.
5
+ * options print a warning instead of raising an error on unknown option
6
+ * The approach introduced in the previous version, of erroring on something people have been accidentally relying on, is too strict in hindsight for a patch version. This was reverted, and instead, a deprecation warning will be printed, to give time for folks to migrate.
7
+ * The format of option messages (exceptions and warnings) also slightly changes to wrap the option in quotes, for proper highlighting.
@@ -10,4 +10,4 @@
10
10
  * `:retries` plugin: resend requests which were piped to a closed connection internally.
11
11
  * `:proxy` plugin: fix issue related with reconnecting inactive connections shut down by the peer, when used with the `:persistent` plugin, where the CONNECT request wasn't being sent to establish the tunnel.
12
12
  * fixed compatibility issue starting with the `json` gem 3.0.0, when calling `HTTPX::Response#json`.
13
- * avoid bookkepping issue in session, where the `on_response_arrived` on a request may be called more than once (which it can happen in the `:follow_redirects` plugin, where a request may have completed due to an error, while the request is waiting to be resent due to `retry-after`).
13
+ * avoid bookkeeping issue in session, where the `on_response_arrived` on a request may be called more than once (which it can happen in the `:follow_redirects` plugin, where a request may have completed due to an error, while the request is waiting to be resent due to `retry-after`).
@@ -4,7 +4,7 @@
4
4
 
5
5
  ### `:hedging` plugin
6
6
 
7
- The `:hedging` implements a strategy called "request hedging", whereby a request is sent a request two times, over different connections, and picking up whichever arrives first.
7
+ The `:hedging` plugin implements a strategy called "request hedging", whereby a request is sent two times, over different connections, and picking up whichever arrives first.
8
8
 
9
9
  ```ruby
10
10
  http = HTTP.plugin(:hedging)
@@ -12,7 +12,7 @@ http.get("http://www.example.com")
12
12
  # sent two times, only the fastest one comes back...
13
13
  ```
14
14
 
15
- ou can read more about it in https://gitlab.com/os85/httpx/wikis/Hedging .
15
+ You can read more about it in https://gitlab.com/os85/httpx/wikis/Hedging .
16
16
 
17
17
  ### `:proxy/http` plugin: new options `:proxy_headers` and `:enable_proxy_tunnel`
18
18
 
@@ -20,7 +20,7 @@ The `:proxy/http` plugin (which gets automatically loaded by the `:proxy` plugin
20
20
 
21
21
  #### `:proxy_headers`
22
22
 
23
- Can be passed a separate set of HTTP headers, which are going to be sent to the proxy when establishing the the CONNECT proxy tunnel.
23
+ Can be passed a separate set of HTTP headers, which are going to be sent to the proxy when establishing the CONNECT proxy tunnel.
24
24
 
25
25
  ```ruby
26
26
  http = HTTPX.plugin(:proxy).with(proxy_headers: { "x-proxy-1" => "foo" }, headers: { "x-header-1" => "bar" }).get("https://example.com")
@@ -48,10 +48,10 @@ When set to `true`, it forces the establishment of a CONNECT proxy tunnel before
48
48
 
49
49
  ## Bugfixes
50
50
 
51
- * `:auth` plugin: fixed clause inverted check, which caused caused a new token to always be generated on retry.
52
- * `:auth` plugin: take auth_header_expires_at into account when deciding to regenerate on retry (token may have been expired by the time the request will be retried).
51
+ * `:auth` plugin: fixed clause inverted check, which caused a new token to always be generated on retry.
52
+ * `:auth` plugin: take auth_header_expires_at into account when deciding to regenerate on retry (token may have expired by the time the request will be retried).
53
53
  * `:tracing` plugin: fixed request initial time calculation in order to take into account the time to wait for a PING correctly.
54
- * `:tracing` plugin: fixed request initial time calculation for pending requests which were waiting for the connect phase and errored meanwhie.
54
+ * `:tracing` plugin: fixed request initial time calculation for pending requests which were waiting for the connect phase and errored meanwhile.
55
55
  * `:proxy/http` plugin: `:headers` will not be sent anymore to the proxy when establishing the CONNECT proxy tunnel.
56
56
  * socket connect timeout bookkeeping is fixed, where in a multi request scenario, connect timeout errors shall not be raised too early if an error happens before the remainder of connections elapses.
57
57
  * `:proxy` plugin: allow proxy params to be frozen, which also fixes an issue where proxies identified as unhealthy by a previous request, would still be retried on for different hostnames.
@@ -72,4 +72,4 @@ When set to `true`, it forces the establishment of a CONNECT proxy tunnel before
72
72
 
73
73
  ## Chore
74
74
 
75
- * Emit warning message whenever connection is deactivated with pending requests; the message contains internal state which should help debug it.
75
+ * Emit a warning message whenever the connection is deactivated with pending requests; the message contains an internal state which should help debug it.
@@ -0,0 +1,32 @@
1
+ # 1.9.1
2
+
3
+ # Improvements
4
+
5
+ * `:connect_timeout` will be split across the IP addresses available for the hostname. The connection phase will then loop on the list, and on each connect timeout, the next IP will tried; the connect timeout error will then only be raised if no healthy connections could be established to any of the IPs. This is the same heuristic used by curl v7.
6
+ * Connections and parsers share the same pending requests queue: before, each had their own, and requests could fly across them depending on the several edge cases, thereby making it needlessly complex and wasteful. Also, when moving requests is necessary, they can short-circuit more efficiently.
7
+ * Handled quoted charset property when parsing the "content-type" header in the response.
8
+ * http1: Support peer server keep alive timeout (exposed via a response header like "Keep-Alive: timeout=10"), which overrides the client (or the default) `:keep_alive_timeout` setting.
9
+ * http2: during the consumption loop, handle more pending requests when the window allows it.
10
+ * several improvements on connection read/write consumption handling which save a few socket read/write syscalls.
11
+
12
+
13
+ ## Bugfixes
14
+
15
+ * fix the json decoding integration with `oj`, which was using the `Oj.load` function in an unsafe manner, which `oj` documents. It now uses the `:strict` mode by default.
16
+ * when doing Happy Eyeballs v2, do not accidentally coalesce sibling connections.
17
+ * on session termination, ensure that idle connections are closed, something which may happen when an `alt-svc` response header creates a new connection, but no subsequent requests are sent its way.
18
+ * fix race condition when terminating a selector, where its connections were checked on its state after termination; because terminated connections get checked back into the pool, they may be checked out before the state check happens. This fixes it by checking its current selector against itself.
19
+ * ensure that the selector only calls the error handling once for the same connection.
20
+ * request timeouts are now only set when the parser accepts them, and guaranteed only once.
21
+ * when an operation timeout occurs, do not raise it on connections for which one wasn't set.
22
+ * native resolver: fix DNS query retry handling due to a confusion about using `Enumerable.first` and `Enumerable.find`, and the former silently ignoring passed blocks.
23
+ * native resolver: discard received answers for DNS queries which have since been dropped due to a timeout.
24
+ * native resolver: try out the next nameserver in case the prior was unreachable.
25
+ * http-2: keep processing unfinished streams when receiving a GOAWAY with a last stream id pointing to streams that haven't been finished.
26
+ * `faraday` adapter: always load the `OnData` plugin, as a streaming request (which uses the `.on_data` Faraday API) may be preceded by a non-streaming request to fetch metadata (a pattern observed when using the google cloud storage ruby SDK).
27
+ * `ntml` and `ntlmv2` plugins: when used to send multiple requests, should return an array of responses.
28
+ * `oauth` plugin: fix wrong method call.
29
+ * `cache` plugin: skip cache flow for error responses.
30
+ * `proxy` plugin: make the class used for the CONNECT proxy request inherit all functionality from other loaded plugins.
31
+ * `fiber_concurrency`: fixed some corner cases where the connection could loop indefinitely or become corrupt.
32
+ * `tracing`: fixing request start time bookkeeping.
@@ -33,7 +33,7 @@ module Faraday
33
33
 
34
34
  @connection = @connection.plugin(:proxy).with(proxy: proxy_options)
35
35
  end
36
- @connection = @connection.plugin(OnDataPlugin) if env.request.stream_response?
36
+ @connection = @connection.plugin(OnDataPlugin)
37
37
 
38
38
  @connection = @config_block.call(@connection) || @connection if @config_block
39
39
  @connection
@@ -20,11 +20,21 @@ module WebMock
20
20
  WebMock::RequestSignature.new(
21
21
  request.verb.downcase.to_sym,
22
22
  uri.to_s,
23
- body: request.body.to_s,
23
+ body: build_webmock_request_body(request),
24
24
  headers: request.headers.to_h
25
25
  )
26
26
  end
27
27
 
28
+ def build_webmock_request_body(request)
29
+ body = request.body
30
+
31
+ return body.to_s unless body.respond_to?(:read)
32
+
33
+ body.read.to_s.tap do
34
+ body.rewind
35
+ end
36
+ end
37
+
28
38
  def build_webmock_response(_request, response)
29
39
  webmock_response = WebMock::Response.new
30
40
  webmock_response.status = [response.status, HTTP_REASONS[response.status]]
@@ -61,6 +71,9 @@ module WebMock
61
71
  def do_init_connection(connection, selector)
62
72
  super
63
73
 
74
+ # skip for connections generated by HEv2 logic.
75
+ return if connection.family
76
+
64
77
  connection.once(:unmock_connection) do
65
78
  next unless connection.current_session == self
66
79
 
@@ -20,14 +20,14 @@ module HTTPX
20
20
 
21
21
  attr_accessor :max_concurrent_requests
22
22
 
23
- def initialize(buffer, options)
23
+ def initialize(buffer, pending, options)
24
24
  @options = options
25
25
  @max_concurrent_requests = @options.max_concurrent_requests || MAX_REQUESTS
26
26
  @max_requests = @options.max_requests
27
27
  @parser = Parser::HTTP1.new(self, options.max_response_headers, options.max_response_header_value_size)
28
28
  @buffer = buffer
29
29
  @version = [1, 1]
30
- @pending = []
30
+ @pending = pending
31
31
  @requests = []
32
32
  @callbacks = @request = nil
33
33
  @handshake_completed = @pipelining = false
@@ -51,21 +51,28 @@ module HTTPX
51
51
  @max_requests = @options.max_requests || MAX_REQUESTS
52
52
  @parser.reset!
53
53
  @handshake_completed = false
54
- reset_requests
55
54
  end
56
55
 
56
+ # move incompleted requests back to pending queue.
57
57
  def reset_requests
58
- @requests.reverse_each do |request|
59
- next if request.response
58
+ completed_idx = 0
59
+ @requests.each_with_index do |request, idx|
60
+ if request.response
61
+ completed_idx = idx + 1
62
+ next
63
+ end
60
64
 
61
65
  request.transition(:idle)
62
- @pending.unshift(request)
63
66
  end
67
+ # drop completed requests
68
+ @requests.shift(completed_idx)
69
+ @pending.unshift(*@requests)
64
70
  @requests.clear
65
71
  end
66
72
 
67
73
  def close
68
74
  reset
75
+ reset_requests
69
76
  emit(:close)
70
77
  end
71
78
 
@@ -88,14 +95,16 @@ module HTTPX
88
95
  @parser << data
89
96
  end
90
97
 
91
- def send(request, head = false)
98
+ def send(request, head = false) # rubocop:disable Naming/PredicateMethod
92
99
  unless @max_requests.positive? && @max_requests > @requests.size
93
100
  head ? @pending.unshift(request) : @pending << request
94
- return
101
+ return false
95
102
  end
96
103
 
97
104
  @requests << request
98
105
  @pipelining = @max_concurrent_requests > 1 && @requests.size > 1
106
+ emit(:request_accepted, request)
107
+ true
99
108
  end
100
109
 
101
110
  def consume
@@ -195,8 +204,7 @@ module HTTPX
195
204
  manage_connection(request, response)
196
205
 
197
206
  if exhausted?
198
- @pending.unshift(*@requests)
199
- @requests.clear
207
+ reset_requests
200
208
 
201
209
  emit(:exhausted)
202
210
  elsif @requests.any?
@@ -237,13 +245,16 @@ module HTTPX
237
245
  def ping
238
246
  reset
239
247
  emit(:reset)
240
- emit(:exhausted)
241
248
  end
242
249
 
243
250
  def waiting_for_ping?
244
251
  false
245
252
  end
246
253
 
254
+ def no_more_requests?
255
+ @requests.empty?
256
+ end
257
+
247
258
  private
248
259
 
249
260
  def manage_connection(request, response)
@@ -252,8 +263,7 @@ module HTTPX
252
263
  when /keep-alive/i
253
264
  if @handshake_completed
254
265
  if @max_requests.zero?
255
- @pending.unshift(*@requests)
256
- @requests.clear
266
+ reset_requests
257
267
  emit(:exhausted)
258
268
  end
259
269
  return
@@ -267,10 +277,7 @@ module HTTPX
267
277
  end]
268
278
  @max_requests = parameters["max"].to_i - 1 if parameters.key?("max")
269
279
 
270
- if parameters.key?("timeout")
271
- keep_alive_timeout = parameters["timeout"].to_i
272
- emit(:timeout, keep_alive_timeout)
273
- end
280
+ emit(:keep_alive_timeout, parameters["timeout"].to_i) if parameters.key?("timeout")
274
281
  @handshake_completed = true
275
282
  when /close/i
276
283
  disable
@@ -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 = []
@@ -74,10 +74,15 @@ module HTTPX
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?
@@ -97,15 +102,10 @@ module HTTPX
97
102
  return :r
98
103
  end
99
104
 
100
- # only wait for writable if we're not waiting on more streams,
101
- # or waiting on ping ACKs.
102
- nothing_to_wait_for = @streams.empty? && @pings.empty?
103
-
104
105
  # there are pending bufferable requests
105
- if !@pending.empty? && can_buffer_more_requests?
106
- # only wait for writable if we're not waiting on more streams,
107
- # or waiting on ping ACKs.
108
- return nothing_to_wait_for ? :w : :rw
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
109
  end
110
110
 
111
111
  unless @drains.empty?
@@ -123,17 +123,47 @@ module HTTPX
123
123
 
124
124
  if @buffer.empty?
125
125
  # skip if no more requests or pings to process
126
- return if nothing_to_wait_for
126
+ return unless waiting_for_peer?
127
127
 
128
128
  :r
129
129
  else
130
130
  # buffered frames
131
- nothing_to_wait_for ? :w : :rw
131
+ waiting_for_peer? ? :rw : :w
132
132
  end
133
133
  end
134
134
 
135
135
  def close
136
- 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
137
167
  @connection.goaway
138
168
  emit(:timeout, @options.timeout.close_handshake_timeout)
139
169
  end
@@ -156,10 +186,16 @@ module HTTPX
156
186
  # data received by the peer.
157
187
  emit_stream_error(stream, request, error)
158
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
159
195
  end
160
196
 
161
197
  def send(request, head = false)
162
- unless can_buffer_more_requests?
198
+ unless can_buffer_pending_requests?
163
199
  head ? @pending.unshift(request) : @pending << request
164
200
  return false
165
201
  end
@@ -168,6 +204,7 @@ module HTTPX
168
204
  handle_stream(stream, request)
169
205
  @streams[request] = stream
170
206
  @max_requests -= 1
207
+ emit(:request_accepted, request)
171
208
  end
172
209
  handle(request, stream)
173
210
  true
@@ -176,6 +213,7 @@ module HTTPX
176
213
  false
177
214
  rescue ::HTTP2::Error::Error, ArgumentError => e
178
215
  emit(:error, request, e)
216
+ false
179
217
  end
180
218
 
181
219
  def consume
@@ -184,6 +222,8 @@ module HTTPX
184
222
 
185
223
  handle(request, stream)
186
224
  end
225
+
226
+ send_pending
187
227
  end
188
228
 
189
229
  def handle_error(ex, request = nil)
@@ -191,37 +231,60 @@ module HTTPX
191
231
  case ex
192
232
  when OperationTimeoutError
193
233
  if !@handshake_completed && @connection.state != :closed
234
+ # timed out waiting for HTTP/2 connection handshake to complete.
194
235
  @connection.goaway(:settings_timeout, "closing due to settings timeout")
195
236
  emit(:close_handshake)
196
237
  settings_ex = SettingsTimeoutError.new(ex.timeout, ex.message)
197
238
  settings_ex.set_backtrace(ex.backtrace)
198
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
199
248
  end
200
249
  when GoawayError
201
250
  last_stream_id = ex.last_stream_id
202
251
  end
203
252
 
204
253
  inflight_unprocessed_requests = [] #: Array[Request]
254
+ unprocessed_streams = [] #: Array[::HTTP2::Stream]
255
+
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
205
259
 
206
- while (req, stream = @streams.shift)
207
260
  @drains.delete(stream)
208
261
 
209
- next if request && request == req
262
+ next(true) if request && request == req
210
263
 
211
264
  if stream.id > last_stream_id
212
265
  req.transition(:idle)
213
266
  # unprocessed request
214
267
  inflight_unprocessed_requests << req
268
+ unprocessed_streams << stream
215
269
 
216
- next
270
+ next(true)
217
271
  end
218
272
 
219
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
220
284
  end
221
285
 
222
286
  if ex.is_a?(GoawayError) && !ex.unrecoverable?
223
287
  # resend unprocessed requests on a different connection
224
- @pending.unshift(*inflight_unprocessed_requests) if inflight_unprocessed_requests.any?
225
288
  emit(:exhausted, ex) if @pending.any?
226
289
  return
227
290
  end
@@ -244,18 +307,29 @@ module HTTPX
244
307
  @pings.any?
245
308
  end
246
309
 
310
+ def no_more_requests?
311
+ @streams.empty?
312
+ end
313
+
247
314
  def reset_requests; end
248
315
 
249
316
  private
250
317
 
251
- def can_buffer_more_requests?
252
- (@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) &&
253
327
  @streams.size < @max_concurrent_requests &&
254
328
  @streams.size < @max_requests
255
329
  end
256
330
 
257
331
  def send_pending
258
- while (request = @pending.shift)
332
+ while !@buffer.full? && can_buffer_pending_requests? && (request = @pending.shift)
259
333
  break unless send(request, true)
260
334
  end
261
335
  end
@@ -527,16 +601,21 @@ module HTTPX
527
601
  emit(:error, request, error)
528
602
  end
529
603
  else
530
- 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)
531
606
  ex.set_backtrace(caller)
532
607
 
533
608
  handle_error(ex)
534
- teardown
535
609
  end
536
610
  end
537
- return unless is_connection_closed && @streams.empty?
611
+ return unless is_connection_closed
538
612
 
539
- 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
540
619
  end
541
620
 
542
621
  def on_frame_sent(frame)