httpx 1.8.3 → 1.9.0
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_4.md +13 -0
- data/doc/release_notes/1_9_0.md +75 -0
- data/lib/httpx/adapters/webmock.rb +1 -1
- data/lib/httpx/altsvc.rb +29 -15
- data/lib/httpx/callbacks.rb +2 -2
- data/lib/httpx/chainable.rb +6 -3
- data/lib/httpx/connection/http1.rb +11 -19
- data/lib/httpx/connection/http2.rb +112 -27
- data/lib/httpx/connection.rb +184 -46
- data/lib/httpx/headers.rb +2 -2
- data/lib/httpx/io/tcp.rb +18 -14
- 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 -0
- data/lib/httpx/plugins/basic_auth.rb +2 -0
- data/lib/httpx/plugins/callbacks.rb +7 -6
- data/lib/httpx/plugins/circuit_breaker.rb +1 -0
- data/lib/httpx/plugins/cookies/cookie.rb +1 -1
- data/lib/httpx/plugins/cookies.rb +2 -2
- data/lib/httpx/plugins/digest_auth.rb +2 -0
- data/lib/httpx/plugins/expect.rb +2 -0
- data/lib/httpx/plugins/fiber_concurrency.rb +6 -0
- data/lib/httpx/plugins/follow_redirects.rb +8 -6
- data/lib/httpx/plugins/grpc/call.rb +4 -4
- data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -2
- data/lib/httpx/plugins/grpc.rb +21 -0
- data/lib/httpx/plugins/hedging.rb +284 -0
- data/lib/httpx/plugins/ntlm_auth.rb +1 -0
- data/lib/httpx/plugins/ntlm_v2_auth.rb +1 -0
- data/lib/httpx/plugins/oauth.rb +1 -1
- data/lib/httpx/plugins/proxy/http.rb +71 -16
- data/lib/httpx/plugins/proxy/ssh.rb +30 -11
- data/lib/httpx/plugins/proxy.rb +66 -45
- data/lib/httpx/plugins/push_promise.rb +4 -2
- data/lib/httpx/plugins/retries.rb +13 -3
- data/lib/httpx/plugins/server_sent_events.rb +2 -1
- data/lib/httpx/plugins/ssrf_filter.rb +1 -1
- data/lib/httpx/plugins/stream.rb +2 -2
- data/lib/httpx/plugins/tracing.rb +61 -14
- data/lib/httpx/plugins/upgrade/h2.rb +1 -1
- 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 +10 -7
- data/lib/httpx/resolver/multi.rb +2 -2
- data/lib/httpx/resolver/resolver.rb +2 -2
- data/lib/httpx/resolver.rb +5 -1
- data/lib/httpx/response/body.rb +2 -2
- data/lib/httpx/response.rb +19 -11
- data/lib/httpx/selector.rb +3 -3
- data/lib/httpx/session.rb +38 -11
- data/lib/httpx/transcoder/chunker.rb +2 -2
- data/lib/httpx/transcoder/json.rb +7 -7
- data/lib/httpx/version.rb +1 -1
- data/sig/altsvc.rbs +3 -3
- data/sig/callbacks.rbs +1 -0
- data/sig/chainable.rbs +1 -0
- data/sig/connection/http1.rbs +1 -1
- data/sig/connection/http2.rbs +30 -16
- data/sig/connection.rbs +19 -1
- data/sig/headers.rbs +1 -1
- data/sig/httpx.rbs +2 -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/hedging.rbs +44 -0
- data/sig/plugins/proxy/http.rbs +8 -1
- data/sig/plugins/proxy.rbs +3 -2
- data/sig/plugins/retries.rbs +2 -2
- data/sig/plugins/tracing.rbs +14 -5
- data/sig/request/body.rbs +1 -1
- data/sig/request.rbs +2 -1
- data/sig/resolver/entry.rbs +2 -2
- data/sig/response.rbs +1 -2
- data/sig/selector.rbs +2 -2
- data/sig/session.rbs +7 -5
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e272ec707fd07e29993d6f5eff50f84f40ad4da61503552f1de2698a8b56b72b
|
|
4
|
+
data.tar.gz: 10ebe441bd62cc1fec49c3caedb7a564dd998d2dd49d0740b3224f583f23e506
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 860445bf67dc3bb69f9724d1dcc13f5a7c21a936420addd0923aa4d955be929afd95b87fb032f5a226d245d65b4e07654b6df431992e755341583a5f0e5c26f9
|
|
7
|
+
data.tar.gz: 4f11cd526c5099fa92d47fbd6b08a4366af6a01f4075f3df96f861cf7f3ce33e17a34e69f436b453e51d8afa07535b2bc62c04e1c1f260942782b0f432cc7383
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 1.8.4
|
|
2
|
+
|
|
3
|
+
## Improvements
|
|
4
|
+
|
|
5
|
+
* send requests which were not sent, or were not processed by the server, when the server sends back a GOAWAY frame (which is deemed recoverable, like when there's no error), after forcing a reconnection (instead of bubbling up the GOAWAY error for all of them).
|
|
6
|
+
|
|
7
|
+
## Bugfixes
|
|
8
|
+
|
|
9
|
+
* `:retries` plugin: also retry requests which failed due to a RST_STREAM frame with REFUSED_STREAM error code.
|
|
10
|
+
* `:retries` plugin: resend requests which were piped to a closed connection internally.
|
|
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
|
+
* 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`).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# 1.9.0
|
|
2
|
+
|
|
3
|
+
## Features
|
|
4
|
+
|
|
5
|
+
### `:hedging` plugin
|
|
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.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
http = HTTP.plugin(:hedging)
|
|
11
|
+
http.get("http://www.example.com")
|
|
12
|
+
# sent two times, only the fastest one comes back...
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
ou can read more about it in https://gitlab.com/os85/httpx/wikis/Hedging .
|
|
16
|
+
|
|
17
|
+
### `:proxy/http` plugin: new options `:proxy_headers` and `:enable_proxy_tunnel`
|
|
18
|
+
|
|
19
|
+
The `:proxy/http` plugin (which gets automatically loaded by the `:proxy` plugin) gains two new options.
|
|
20
|
+
|
|
21
|
+
#### `:proxy_headers`
|
|
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.
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
http = HTTPX.plugin(:proxy).with(proxy_headers: { "x-proxy-1" => "foo" }, headers: { "x-header-1" => "bar" }).get("https://example.com")
|
|
27
|
+
# -> CONNECT example.com HTTP/1.1
|
|
28
|
+
# -> Host: example.com
|
|
29
|
+
# -> X-Proxy-1: foo
|
|
30
|
+
# ...
|
|
31
|
+
# <- 200 OK
|
|
32
|
+
# ...
|
|
33
|
+
# -> GET / HTTP/1.1
|
|
34
|
+
# -> X-Headers-1: bar
|
|
35
|
+
# ...
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### `:enable_proxy_tunnel`
|
|
39
|
+
|
|
40
|
+
When set to `true`, it forces the establishment of a CONNECT proxy tunnel before sending the request. Bear in mind that this will be the norm for `https://` requests, so only use this in cases where the requests are to be sent to an `http://` URL and a CONNECT proxy tunnel is required.
|
|
41
|
+
|
|
42
|
+
## Improvements
|
|
43
|
+
|
|
44
|
+
* `:request_timeout` will also observe the connect phase, in case a request is sent to an idle connection. This means that, if you're using `:request_timeout`, you no longer need to specify `:connect_timeout`.
|
|
45
|
+
* The same will happen if you're using `:total_request_timeout`.
|
|
46
|
+
* do not require "io/wait" anymore for ruby 3.2 or higher.
|
|
47
|
+
* `:proxy/http` plugin: raise a `HTTPX::ConnectTimeoutError` if there was a timeout error while waiting for the CONNECT request to complete.
|
|
48
|
+
|
|
49
|
+
## Bugfixes
|
|
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).
|
|
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.
|
|
55
|
+
* `:proxy/http` plugin: `:headers` will not be sent anymore to the proxy when establishing the CONNECT proxy tunnel.
|
|
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
|
+
* `: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.
|
|
58
|
+
* `:callbacks` plugin: fix broken visibility of session `#close` method.
|
|
59
|
+
* native resolver: retry dns queries on not-imp and refused errors (treats them as transient resolver errors).
|
|
60
|
+
* `webmock` adapter: fix emission of `:response` callback so that this only happens after the body is fully buffered (which makes the `:callbacks` plugin `.on_response_completed` callback works the same way when and when not stubbed).
|
|
61
|
+
* `:addresses`: fix support for the documented variant where a hostname/ips hash is passed.
|
|
62
|
+
* `http2`: fix interests calculation, so that:
|
|
63
|
+
* a connection expects to read from connections which still have in-transit streams.
|
|
64
|
+
* an HTTP/2 with no available flow control window in its streams will wait for incoming WINDOW_UPDATE frames.
|
|
65
|
+
* `http2`: pause buffering the request body if there's no available control window in the corresponding stream or connection.
|
|
66
|
+
* `http2`: reset the ping timer too if a connection receives a GOAWAY frame while waiting for a PING to be ack'ed.
|
|
67
|
+
* wait for read or write for any operation, considering that `OpenSSL::SSL::SSLSocket` objects may return both `.wait_readable` and `:wait_writable` for reads and writes.
|
|
68
|
+
* when using `:max_requests`, also account for inflight requests when deciding to send it to the parser or the pending queue.
|
|
69
|
+
* fixed `HTTPX::Options#merge` which was not freezing the resulting options object, something which happens when enabling plugins, which made resulting sessions unusable across Ractors.
|
|
70
|
+
* fixed the `Alt-Svc` header parser to be able to parse values with multiple authorities.
|
|
71
|
+
* fixed request params handling so that they are passed to all requests invoked (instead of only the first of them).
|
|
72
|
+
|
|
73
|
+
## Chore
|
|
74
|
+
|
|
75
|
+
* Emit warning message whenever connection is deactivated with pending requests; the message contains internal state which should help debug it.
|
|
@@ -133,10 +133,10 @@ module WebMock
|
|
|
133
133
|
request.transition(:done)
|
|
134
134
|
response.finish!
|
|
135
135
|
request.response = response
|
|
136
|
+
response << mock_response.body.dup unless response.is_a?(HTTPX::ErrorResponse)
|
|
136
137
|
request.emit_response(response)
|
|
137
138
|
request_signature.headers = request.headers.to_h
|
|
138
139
|
|
|
139
|
-
response << mock_response.body.dup unless response.is_a?(HTTPX::ErrorResponse)
|
|
140
140
|
elsif WebMock.net_connect_allowed?(request_signature.uri)
|
|
141
141
|
if WebMock::CallbackRegistry.any_callbacks?
|
|
142
142
|
request.on(:response) do |resp|
|
data/lib/httpx/altsvc.rb
CHANGED
|
@@ -10,7 +10,7 @@ module HTTPX
|
|
|
10
10
|
|
|
11
11
|
H2_ALTSVC_SCHEMES = %w[https h2].freeze
|
|
12
12
|
|
|
13
|
-
ALTSVC_IGNORE_IVARS = %i[
|
|
13
|
+
ALTSVC_IGNORE_IVARS = %i[ssl].freeze
|
|
14
14
|
|
|
15
15
|
def send(request)
|
|
16
16
|
request.headers["alt-used"] = @origin.authority if @parser && !@write_buffer.full? && match_altsvcs?(request.uri)
|
|
@@ -30,7 +30,7 @@ module HTTPX
|
|
|
30
30
|
# +uri+
|
|
31
31
|
def match_altsvcs?(uri)
|
|
32
32
|
@origins.any? { |origin| altsvc_match?(uri, origin) } ||
|
|
33
|
-
AltSvc.cached_altsvc(@origin).any? do |altsvc|
|
|
33
|
+
AltSvc.cached_altsvc(@origin.to_s).any? do |altsvc|
|
|
34
34
|
origin = altsvc["origin"]
|
|
35
35
|
altsvc_match?(origin, uri.origin)
|
|
36
36
|
end
|
|
@@ -115,33 +115,45 @@ module HTTPX
|
|
|
115
115
|
return
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
parse(altsvc) do |alt_origin, alt_params|
|
|
118
|
+
parse(altsvc, host: host) do |alt_origin, alt_params|
|
|
119
119
|
alt_origin.host ||= host
|
|
120
120
|
yield(alt_origin, origin, alt_params)
|
|
121
121
|
end
|
|
122
122
|
end
|
|
123
123
|
|
|
124
|
-
def parse(altsvc)
|
|
125
|
-
return enum_for(__method__, altsvc) unless block_given?
|
|
124
|
+
def parse(altsvc, host: nil)
|
|
125
|
+
return enum_for(__method__, altsvc, host: host) unless block_given?
|
|
126
126
|
|
|
127
127
|
scanner = StringScanner.new(altsvc)
|
|
128
128
|
until scanner.eos?
|
|
129
|
-
|
|
129
|
+
alt_services = [] #: Array[String]
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
while (alt_service = scanner.scan(/[^=]+=("[^"]+")/))
|
|
132
|
+
alt_services << alt_service
|
|
133
|
+
scanner.skip(/[, ]+/)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
scanner.skip(/;/)
|
|
137
|
+
|
|
138
|
+
alt_params = [] #: Array[String]
|
|
132
139
|
loop do
|
|
133
140
|
alt_param = scanner.scan(/[^=]+=("[^"]+"|[^;,]+)/)
|
|
134
141
|
alt_params << alt_param.strip if alt_param
|
|
135
142
|
scanner.skip(/;/)
|
|
136
143
|
break if scanner.eos? || scanner.scan(/ *, */)
|
|
137
144
|
end
|
|
138
|
-
alt_params = Hash[alt_params.map
|
|
145
|
+
alt_params = Hash[alt_params.map do |field|
|
|
146
|
+
field.split("=", 2)
|
|
147
|
+
end]
|
|
139
148
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
149
|
+
alt_services.each do |alt_service|
|
|
150
|
+
alt_proto, alt_authority = alt_service.split("=", 2)
|
|
151
|
+
alt_origin = parse_altsvc_origin(alt_proto, alt_authority, host: host)
|
|
143
152
|
|
|
144
|
-
|
|
153
|
+
next unless alt_origin
|
|
154
|
+
|
|
155
|
+
yield(alt_origin, alt_params.merge("proto" => alt_proto))
|
|
156
|
+
end
|
|
145
157
|
end
|
|
146
158
|
end
|
|
147
159
|
|
|
@@ -149,19 +161,21 @@ module HTTPX
|
|
|
149
161
|
case alt_proto
|
|
150
162
|
when "h2c"
|
|
151
163
|
"http"
|
|
152
|
-
when "h2"
|
|
164
|
+
when "h2", "quic", /^h3/
|
|
153
165
|
"https"
|
|
154
166
|
end
|
|
155
167
|
end
|
|
156
168
|
|
|
157
|
-
def parse_altsvc_origin(alt_proto, alt_origin)
|
|
169
|
+
def parse_altsvc_origin(alt_proto, alt_origin, host: nil)
|
|
158
170
|
alt_scheme = parse_altsvc_scheme(alt_proto)
|
|
159
171
|
|
|
160
172
|
return unless alt_scheme
|
|
161
173
|
|
|
162
174
|
alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
|
|
163
175
|
|
|
164
|
-
URI.parse("#{alt_scheme}://#{alt_origin}")
|
|
176
|
+
uri = URI.parse("#{alt_scheme}://#{alt_origin}")
|
|
177
|
+
uri.host = host if uri.host.nil? || uri.host.empty?
|
|
178
|
+
uri
|
|
165
179
|
end
|
|
166
180
|
end
|
|
167
181
|
end
|
data/lib/httpx/callbacks.rb
CHANGED
|
@@ -14,9 +14,9 @@ module HTTPX
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def emit(type,
|
|
17
|
+
def emit(type, ...)
|
|
18
18
|
log { "emit #{type.inspect} callbacks" } if respond_to?(:log)
|
|
19
|
-
callbacks(type).delete_if { |pr| :delete == pr.call(
|
|
19
|
+
callbacks(type).delete_if { |pr| :delete == pr.call(...) } # rubocop:disable Style/YodaCondition
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def callbacks_for?(type)
|
data/lib/httpx/chainable.rb
CHANGED
|
@@ -5,18 +5,21 @@ module HTTPX
|
|
|
5
5
|
# delegates to a default session when extended.
|
|
6
6
|
module Chainable
|
|
7
7
|
%w[head get post put delete trace options connect patch].each do |meth|
|
|
8
|
+
http_verb = meth.upcase
|
|
8
9
|
class_eval(<<-MOD, __FILE__, __LINE__ + 1)
|
|
10
|
+
# sends a #{http_verb} request for the given +uri+, using +options+ for each.
|
|
9
11
|
def #{meth}(*uri, **options) # def get(*uri, **options)
|
|
10
|
-
request("#{
|
|
12
|
+
request("#{http_verb}", uri, **options) # request("GET", uri, **options)
|
|
11
13
|
end # end
|
|
12
14
|
MOD
|
|
13
15
|
end
|
|
14
16
|
|
|
15
17
|
# delegates to the default session (see HTTPX::Session#request).
|
|
16
|
-
def request(
|
|
17
|
-
branch(default_options).request(
|
|
18
|
+
def request(...)
|
|
19
|
+
branch(default_options).request(...)
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
# returns a new session with the "Accept" header set to +type+.
|
|
20
23
|
def accept(type)
|
|
21
24
|
with(headers: { "accept" => String(type) })
|
|
22
25
|
end
|
|
@@ -34,7 +34,7 @@ module HTTPX
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def timeout
|
|
37
|
-
@options.timeout
|
|
37
|
+
@options.timeout.operation_timeout
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def interests
|
|
@@ -48,10 +48,6 @@ module HTTPX
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def reset
|
|
51
|
-
if @ping_timer
|
|
52
|
-
@ping_timer.cancel
|
|
53
|
-
@ping_timer = nil
|
|
54
|
-
end
|
|
55
51
|
@max_requests = @options.max_requests || MAX_REQUESTS
|
|
56
52
|
@parser.reset!
|
|
57
53
|
@handshake_completed = false
|
|
@@ -92,14 +88,12 @@ module HTTPX
|
|
|
92
88
|
@parser << data
|
|
93
89
|
end
|
|
94
90
|
|
|
95
|
-
def send(request)
|
|
96
|
-
unless @max_requests.positive?
|
|
97
|
-
@pending << request
|
|
91
|
+
def send(request, head = false)
|
|
92
|
+
unless @max_requests.positive? && @max_requests > @requests.size
|
|
93
|
+
head ? @pending.unshift(request) : @pending << request
|
|
98
94
|
return
|
|
99
95
|
end
|
|
100
96
|
|
|
101
|
-
return if @requests.include?(request)
|
|
102
|
-
|
|
103
97
|
@requests << request
|
|
104
98
|
@pipelining = @max_concurrent_requests > 1 && @requests.size > 1
|
|
105
99
|
end
|
|
@@ -132,9 +126,9 @@ module HTTPX
|
|
|
132
126
|
headers = request.options.headers_class.new(h)
|
|
133
127
|
response = request.options.response_class.new(request,
|
|
134
128
|
@parser.status_code,
|
|
135
|
-
@parser.http_version
|
|
129
|
+
@parser.http_version,
|
|
136
130
|
headers)
|
|
137
|
-
request.log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version
|
|
131
|
+
request.log(color: :yellow) { "-> HEADLINE: #{response.status} HTTP/#{@parser.http_version}" }
|
|
138
132
|
request.log(color: :yellow) { response.headers.each.map { |f, v| "-> HEADER: #{f}: #{log_redact_headers(v)}" }.join("\n") }
|
|
139
133
|
|
|
140
134
|
if response.content_length && response.content_length > request.options.max_response_body_size
|
|
@@ -187,7 +181,7 @@ module HTTPX
|
|
|
187
181
|
|
|
188
182
|
@request = nil
|
|
189
183
|
@requests.shift
|
|
190
|
-
response = request.response
|
|
184
|
+
response = request.response #: Response
|
|
191
185
|
emit(:response, request, response)
|
|
192
186
|
|
|
193
187
|
if @parser.upgrade?
|
|
@@ -198,19 +192,17 @@ module HTTPX
|
|
|
198
192
|
|
|
199
193
|
@parser.reset!
|
|
200
194
|
@max_requests -= 1
|
|
201
|
-
|
|
202
|
-
disable
|
|
203
|
-
else
|
|
204
|
-
manage_connection(request, response)
|
|
205
|
-
end
|
|
195
|
+
manage_connection(request, response)
|
|
206
196
|
|
|
207
197
|
if exhausted?
|
|
208
198
|
@pending.unshift(*@requests)
|
|
209
199
|
@requests.clear
|
|
210
200
|
|
|
211
201
|
emit(:exhausted)
|
|
202
|
+
elsif @requests.any?
|
|
203
|
+
consume
|
|
212
204
|
else
|
|
213
|
-
send(@pending.shift) unless @pending.empty?
|
|
205
|
+
send(@pending.shift, true) unless @pending.empty?
|
|
214
206
|
end
|
|
215
207
|
end
|
|
216
208
|
|
|
@@ -23,9 +23,29 @@ module HTTPX
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
class GoawayError < Error
|
|
26
|
-
|
|
26
|
+
UNRECOVERABLE_ERRORS = %i[settings_timeout inadequate_security].freeze
|
|
27
|
+
|
|
28
|
+
def initialize(code, last_stream_id)
|
|
29
|
+
@code = code
|
|
30
|
+
@last_stream_id = last_stream_id
|
|
27
31
|
super(0, code)
|
|
28
32
|
end
|
|
33
|
+
|
|
34
|
+
def last_stream_id
|
|
35
|
+
return Float::INFINITY if unrecoverable?
|
|
36
|
+
|
|
37
|
+
@last_stream_id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def unrecoverable?
|
|
41
|
+
UNRECOVERABLE_ERRORS.include?(@code)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class RstStreamError < Error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class RefusedStreamError < RstStreamError
|
|
29
49
|
end
|
|
30
50
|
|
|
31
51
|
attr_reader :streams, :pending
|
|
@@ -48,9 +68,9 @@ module HTTPX
|
|
|
48
68
|
end
|
|
49
69
|
|
|
50
70
|
def timeout
|
|
51
|
-
return @options.timeout
|
|
71
|
+
return @options.timeout.operation_timeout if @handshake_completed
|
|
52
72
|
|
|
53
|
-
@options.timeout
|
|
73
|
+
@options.timeout.settings_timeout
|
|
54
74
|
end
|
|
55
75
|
|
|
56
76
|
def interests
|
|
@@ -77,27 +97,45 @@ module HTTPX
|
|
|
77
97
|
return :r
|
|
78
98
|
end
|
|
79
99
|
|
|
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
|
+
|
|
80
104
|
# there are pending bufferable requests
|
|
81
|
-
|
|
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
|
|
109
|
+
end
|
|
82
110
|
|
|
83
|
-
|
|
84
|
-
|
|
111
|
+
unless @drains.empty?
|
|
112
|
+
return :w unless @buffer.empty?
|
|
113
|
+
|
|
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
|
|
85
123
|
|
|
86
124
|
if @buffer.empty?
|
|
87
125
|
# skip if no more requests or pings to process
|
|
88
|
-
return if
|
|
126
|
+
return if nothing_to_wait_for
|
|
89
127
|
|
|
90
128
|
:r
|
|
91
129
|
else
|
|
92
130
|
# buffered frames
|
|
93
|
-
:w
|
|
131
|
+
nothing_to_wait_for ? :w : :rw
|
|
94
132
|
end
|
|
95
133
|
end
|
|
96
134
|
|
|
97
135
|
def close
|
|
98
136
|
unless @connection.closed?
|
|
99
137
|
@connection.goaway
|
|
100
|
-
emit(:timeout, @options.timeout
|
|
138
|
+
emit(:timeout, @options.timeout.close_handshake_timeout)
|
|
101
139
|
end
|
|
102
140
|
emit(:close)
|
|
103
141
|
end
|
|
@@ -149,18 +187,45 @@ module HTTPX
|
|
|
149
187
|
end
|
|
150
188
|
|
|
151
189
|
def handle_error(ex, request = nil)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
190
|
+
last_stream_id = Float::INFINITY
|
|
191
|
+
case ex
|
|
192
|
+
when OperationTimeoutError
|
|
193
|
+
if !@handshake_completed && @connection.state != :closed
|
|
194
|
+
@connection.goaway(:settings_timeout, "closing due to settings timeout")
|
|
195
|
+
emit(:close_handshake)
|
|
196
|
+
settings_ex = SettingsTimeoutError.new(ex.timeout, ex.message)
|
|
197
|
+
settings_ex.set_backtrace(ex.backtrace)
|
|
198
|
+
ex = settings_ex
|
|
199
|
+
end
|
|
200
|
+
when GoawayError
|
|
201
|
+
last_stream_id = ex.last_stream_id
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
inflight_unprocessed_requests = [] #: Array[Request]
|
|
205
|
+
|
|
206
|
+
while (req, stream = @streams.shift)
|
|
207
|
+
@drains.delete(stream)
|
|
208
|
+
|
|
160
209
|
next if request && request == req
|
|
161
210
|
|
|
211
|
+
if stream.id > last_stream_id
|
|
212
|
+
req.transition(:idle)
|
|
213
|
+
# unprocessed request
|
|
214
|
+
inflight_unprocessed_requests << req
|
|
215
|
+
|
|
216
|
+
next
|
|
217
|
+
end
|
|
218
|
+
|
|
162
219
|
emit(:error, req, ex)
|
|
163
220
|
end
|
|
221
|
+
|
|
222
|
+
if ex.is_a?(GoawayError) && !ex.unrecoverable?
|
|
223
|
+
# resend unprocessed requests on a different connection
|
|
224
|
+
@pending.unshift(*inflight_unprocessed_requests) if inflight_unprocessed_requests.any?
|
|
225
|
+
emit(:exhausted, ex) if @pending.any?
|
|
226
|
+
return
|
|
227
|
+
end
|
|
228
|
+
|
|
164
229
|
while (req = @pending.shift)
|
|
165
230
|
next if request && request == req
|
|
166
231
|
|
|
@@ -277,13 +342,23 @@ module HTTPX
|
|
|
277
342
|
def join_body(stream, request)
|
|
278
343
|
return if request.body.empty?
|
|
279
344
|
|
|
280
|
-
chunk
|
|
345
|
+
# the next chunk waits until the peer grants more window.
|
|
346
|
+
throw(:buffer_full) if @drains.key?(stream) && (!stream.send_buffer.empty? || !@connection.send_buffer.empty?)
|
|
347
|
+
|
|
348
|
+
chunk = @drains.delete(stream) || request.drain_body
|
|
281
349
|
while chunk
|
|
282
350
|
next_chunk = request.drain_body
|
|
283
351
|
send_chunk(request, stream, chunk, next_chunk)
|
|
284
352
|
|
|
285
|
-
if next_chunk && (
|
|
286
|
-
|
|
353
|
+
if next_chunk && (
|
|
354
|
+
# send window is full
|
|
355
|
+
@buffer.full? ||
|
|
356
|
+
# stream / chunked request, must write a chunk at a time
|
|
357
|
+
request.body.unbounded_body? ||
|
|
358
|
+
# peer window is full, must wait for WINDOW_UPDATE frame(s)
|
|
359
|
+
!stream.send_buffer.empty? || !@connection.send_buffer.empty?
|
|
360
|
+
)
|
|
361
|
+
@drains[stream] = next_chunk
|
|
287
362
|
throw(:buffer_full)
|
|
288
363
|
end
|
|
289
364
|
|
|
@@ -395,7 +470,14 @@ module HTTPX
|
|
|
395
470
|
when :http_1_1_required
|
|
396
471
|
emit(:error, request, error)
|
|
397
472
|
else
|
|
398
|
-
ex =
|
|
473
|
+
ex =
|
|
474
|
+
case error
|
|
475
|
+
when :refused_stream
|
|
476
|
+
RefusedStreamError.new(stream.id, error)
|
|
477
|
+
else
|
|
478
|
+
RstStreamError.new(stream.id, error)
|
|
479
|
+
end
|
|
480
|
+
|
|
399
481
|
ex.set_backtrace(caller)
|
|
400
482
|
response = ErrorResponse.new(request, ex)
|
|
401
483
|
request.response = response
|
|
@@ -410,7 +492,10 @@ module HTTPX
|
|
|
410
492
|
end
|
|
411
493
|
end
|
|
412
494
|
end
|
|
413
|
-
|
|
495
|
+
|
|
496
|
+
consume
|
|
497
|
+
|
|
498
|
+
send(@pending.shift, true) unless @pending.empty?
|
|
414
499
|
|
|
415
500
|
return unless @streams.empty? && exhausted?
|
|
416
501
|
|
|
@@ -432,7 +517,7 @@ module HTTPX
|
|
|
432
517
|
send_pending
|
|
433
518
|
end
|
|
434
519
|
|
|
435
|
-
def on_close(
|
|
520
|
+
def on_close(last_stream_id, error, _payload)
|
|
436
521
|
is_connection_closed = @connection.closed?
|
|
437
522
|
if error
|
|
438
523
|
@buffer.clear if is_connection_closed
|
|
@@ -442,12 +527,11 @@ module HTTPX
|
|
|
442
527
|
emit(:error, request, error)
|
|
443
528
|
end
|
|
444
529
|
else
|
|
445
|
-
ex = GoawayError.new(error)
|
|
530
|
+
ex = GoawayError.new(error, last_stream_id)
|
|
446
531
|
ex.set_backtrace(caller)
|
|
447
532
|
|
|
448
533
|
handle_error(ex)
|
|
449
534
|
teardown
|
|
450
|
-
|
|
451
535
|
end
|
|
452
536
|
end
|
|
453
537
|
return unless is_connection_closed && @streams.empty?
|
|
@@ -539,8 +623,9 @@ module HTTPX
|
|
|
539
623
|
|
|
540
624
|
def teardown(request = nil)
|
|
541
625
|
if request
|
|
542
|
-
@
|
|
543
|
-
|
|
626
|
+
if (stream = @streams.delete(request))
|
|
627
|
+
@drains.delete(stream)
|
|
628
|
+
end
|
|
544
629
|
else
|
|
545
630
|
@drains.clear
|
|
546
631
|
@streams.clear
|