async-http 0.101.0 → 0.102.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
- checksums.yaml.gz.sig +0 -0
- data/context/choosing-a-client.md +164 -0
- data/context/concurrent-requests.md +134 -0
- data/context/getting-started.md +106 -69
- data/context/index.yaml +14 -3
- data/context/testing.md +173 -45
- data/lib/async/http/body/hijack.rb +1 -1
- data/lib/async/http/body/pipe.rb +2 -2
- data/lib/async/http/body.rb +1 -1
- data/lib/async/http/internet.rb +1 -1
- data/lib/async/http/middleware/location_redirector.rb +1 -1
- data/lib/async/http/mock/endpoint.rb +1 -1
- data/lib/async/http/protocol/configurable.rb +1 -1
- data/lib/async/http/protocol/http.rb +1 -1
- data/lib/async/http/protocol/http1/finishable.rb +2 -1
- data/lib/async/http/protocol/http1/request.rb +1 -1
- data/lib/async/http/protocol/http1/response.rb +1 -1
- data/lib/async/http/protocol/http1.rb +1 -1
- data/lib/async/http/protocol/http10.rb +1 -1
- data/lib/async/http/protocol/http11.rb +1 -1
- data/lib/async/http/protocol/http2/client.rb +7 -2
- data/lib/async/http/protocol/http2/connection.rb +20 -6
- data/lib/async/http/protocol/http2/input.rb +14 -3
- data/lib/async/http/protocol/http2/output.rb +23 -8
- data/lib/async/http/protocol/http2/response.rb +15 -2
- data/lib/async/http/protocol/http2/server.rb +1 -1
- data/lib/async/http/protocol/http2/stream.rb +61 -4
- data/lib/async/http/protocol/http2.rb +1 -1
- data/lib/async/http/protocol/request.rb +1 -1
- data/lib/async/http/protocol/response.rb +1 -1
- data/lib/async/http/proxy.rb +1 -1
- data/lib/async/http/statistics.rb +1 -1
- data/lib/async/http/version.rb +1 -1
- data/lib/async/http.rb +1 -1
- data/lib/traces/provider/async/http/client.rb +1 -1
- data/lib/traces/provider/async/http/protocol/http1/client.rb +1 -1
- data/lib/traces/provider/async/http/protocol/http2/client.rb +1 -1
- data/lib/traces/provider/async/http/server.rb +1 -1
- data/license.md +2 -1
- data/readme.md +18 -13
- data/releases.md +5 -0
- data.tar.gz.sig +0 -0
- metadata +11 -6
- metadata.gz.sig +0 -0
data/context/testing.md
CHANGED
|
@@ -1,77 +1,205 @@
|
|
|
1
1
|
# Testing
|
|
2
2
|
|
|
3
|
-
This guide explains how to
|
|
3
|
+
This guide explains how to test `Async::HTTP` clients and servers without depending on external HTTP services.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Real network services make tests slower and less deterministic. Prefer one of these approaches:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- Use [`sus-fixtures-protocol-http`](https://socketry.github.io/sus-fixtures-protocol-http/guides/getting-started/) to exercise HTTP middleware directly without a client or server.
|
|
8
|
+
- Use `sus-fixtures-async-http` to run an application with a managed local server and client.
|
|
9
|
+
- Use ruby:`Async::HTTP::Mock::Endpoint` when testing a client that expects to connect to a particular remote endpoint.
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## Testing Middleware Directly
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
When a test only needs to construct requests and inspect responses, `sus-fixtures-protocol-http` can call `Protocol::HTTP` middleware in-process without starting a client or server. Add the fixture to your test dependencies:
|
|
14
|
+
|
|
15
|
+
~~~ bash
|
|
16
|
+
$ bundle add sus --group test
|
|
17
|
+
$ bundle add sus-fixtures-protocol-http --group test
|
|
18
|
+
~~~
|
|
19
|
+
|
|
20
|
+
Include `MiddlewareContext` and provide the middleware under test:
|
|
12
21
|
|
|
13
22
|
~~~ ruby
|
|
14
|
-
require
|
|
15
|
-
|
|
23
|
+
require "sus/fixtures/protocol/http/middleware_context"
|
|
24
|
+
|
|
25
|
+
describe "My HTTP application" do
|
|
26
|
+
include Sus::Fixtures::Protocol::HTTP::MiddlewareContext
|
|
27
|
+
|
|
28
|
+
let(:middleware) do
|
|
29
|
+
Protocol::HTTP::Middleware.for do |request|
|
|
30
|
+
Protocol::HTTP::Response[200, {}, ["Hello #{request.path}"]]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "handles a request directly" do
|
|
35
|
+
response = client.get("/world")
|
|
36
|
+
|
|
37
|
+
expect(response.status).to be == 200
|
|
38
|
+
expect(response.read).to be == "Hello /world"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
~~~
|
|
42
|
+
|
|
43
|
+
The fixture closes the final request, response, and middleware after each test. Use `sus-fixtures-async-http` when the test needs a real client/server exchange.
|
|
16
44
|
|
|
17
|
-
|
|
45
|
+
## Testing with a Client and Server
|
|
18
46
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
The `ServerContext` fixture manages an ephemeral listening endpoint, server task, and connected client. Add the fixture to your test dependencies:
|
|
48
|
+
|
|
49
|
+
~~~ bash
|
|
50
|
+
$ bundle add sus --group test
|
|
51
|
+
$ bundle add sus-fixtures-async-http --group test
|
|
52
|
+
~~~
|
|
53
|
+
|
|
54
|
+
Define the application under test and make requests through the provided `client`:
|
|
55
|
+
|
|
56
|
+
~~~ ruby
|
|
57
|
+
require "sus/fixtures/async/http"
|
|
58
|
+
|
|
59
|
+
describe "My HTTP application" do
|
|
60
|
+
include Sus::Fixtures::Async::HTTP::ServerContext
|
|
61
|
+
|
|
62
|
+
let(:app) do
|
|
63
|
+
Protocol::HTTP::Middleware.for do |request|
|
|
64
|
+
case request.path
|
|
65
|
+
when "/health"
|
|
66
|
+
Protocol::HTTP::Response[
|
|
67
|
+
200,
|
|
68
|
+
{"content-type" => "application/json"},
|
|
69
|
+
['{"status":"ok"}'],
|
|
70
|
+
]
|
|
71
|
+
else
|
|
72
|
+
Protocol::HTTP::Response[404, {}, ["Not Found"]]
|
|
73
|
+
end
|
|
25
74
|
end
|
|
26
75
|
end
|
|
27
76
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
77
|
+
it "serves the health endpoint" do
|
|
78
|
+
response = client.get("/health")
|
|
79
|
+
|
|
80
|
+
expect(response).to be(:success?)
|
|
81
|
+
expect(response.headers["content-type"]).to be == "application/json"
|
|
82
|
+
expect(response.read).to be == '{"status":"ok"}'
|
|
83
|
+
ensure
|
|
84
|
+
response&.close
|
|
85
|
+
end
|
|
31
86
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
87
|
+
it "returns not found for unknown paths" do
|
|
88
|
+
response = client.get("/missing")
|
|
89
|
+
expect(response.status).to be == 404
|
|
90
|
+
ensure
|
|
91
|
+
response&.close
|
|
92
|
+
end
|
|
35
93
|
end
|
|
36
94
|
~~~
|
|
37
95
|
|
|
38
|
-
|
|
96
|
+
The fixture closes the client, stops the server, and releases the bound endpoint after each test. Override `app`, `url`, `protocol`, `endpoint_options`, or `retries` to configure a scenario.
|
|
39
97
|
|
|
40
|
-
|
|
98
|
+
### Testing HTTP/2
|
|
41
99
|
|
|
42
|
-
|
|
100
|
+
Override `protocol` when behavior must be verified with a specific HTTP version:
|
|
43
101
|
|
|
44
102
|
~~~ ruby
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
103
|
+
describe "My HTTP/2 application" do
|
|
104
|
+
include Sus::Fixtures::Async::HTTP::ServerContext
|
|
105
|
+
|
|
106
|
+
let(:protocol) {Async::HTTP::Protocol::HTTP2}
|
|
107
|
+
|
|
108
|
+
it "responds using HTTP/2" do
|
|
109
|
+
response = client.get("/")
|
|
110
|
+
expect(response.version).to be == "HTTP/2"
|
|
111
|
+
ensure
|
|
112
|
+
response&.close
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
~~~
|
|
116
|
+
|
|
117
|
+
Test normal behavior without forcing a protocol unless the distinction is relevant to the feature under test.
|
|
48
118
|
|
|
49
|
-
|
|
119
|
+
## Testing a Client with a Mock Endpoint
|
|
50
120
|
|
|
51
|
-
|
|
121
|
+
ruby:`Async::HTTP::Mock::Endpoint` connects the real client and server protocol implementations through a local socket pair. It does not open a network port, but requests still exercise serialization, connection handling, and response bodies.
|
|
122
|
+
|
|
123
|
+
Use ruby:`Async::HTTP::Mock::Endpoint#wrap` to preserve the scheme and authority expected by the client:
|
|
124
|
+
|
|
125
|
+
~~~ ruby
|
|
126
|
+
require "async/http"
|
|
127
|
+
require "async/http/mock"
|
|
128
|
+
require "sus/fixtures/async/reactor_context"
|
|
52
129
|
|
|
53
|
-
|
|
54
|
-
|
|
130
|
+
describe "A remote service client" do
|
|
131
|
+
include Sus::Fixtures::Async::ReactorContext
|
|
55
132
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
133
|
+
it "handles a successful response" do
|
|
134
|
+
mock_endpoint = Async::HTTP::Mock::Endpoint.new
|
|
135
|
+
server_task = Async do
|
|
136
|
+
mock_endpoint.run do |request|
|
|
137
|
+
Protocol::HTTP::Response[200, {}, ["Authority: #{request.authority}"]]
|
|
138
|
+
end
|
|
60
139
|
end
|
|
140
|
+
|
|
141
|
+
remote_endpoint = Async::HTTP::Endpoint.parse("https://api.example.com")
|
|
142
|
+
client = Async::HTTP::Client.new(mock_endpoint.wrap(remote_endpoint))
|
|
143
|
+
response = client.get("/status")
|
|
144
|
+
|
|
145
|
+
expect(response.read).to be == "Authority: api.example.com"
|
|
146
|
+
ensure
|
|
147
|
+
response&.close
|
|
148
|
+
client&.close
|
|
149
|
+
server_task&.stop
|
|
61
150
|
end
|
|
151
|
+
end
|
|
152
|
+
~~~
|
|
153
|
+
|
|
154
|
+
Return different statuses, headers, bodies, delays, or malformed behavior from the mock server to exercise client error handling.
|
|
155
|
+
|
|
156
|
+
## Transparently Replacing Client Endpoints
|
|
157
|
+
|
|
158
|
+
Some applications construct ruby:`Async::HTTP::Client` internally. A test can wrap the constructor so those clients connect to a mock endpoint while retaining the original endpoint metadata and client options:
|
|
159
|
+
|
|
160
|
+
~~~ ruby
|
|
161
|
+
require "async/http"
|
|
162
|
+
require "async/http/mock"
|
|
163
|
+
require "sus/fixtures/async/reactor_context"
|
|
164
|
+
|
|
165
|
+
describe "A client created by application code" do
|
|
166
|
+
include Sus::Fixtures::Async::ReactorContext
|
|
167
|
+
|
|
168
|
+
let(:mock_endpoint) {Async::HTTP::Mock::Endpoint.new}
|
|
62
169
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
170
|
+
def before
|
|
171
|
+
super
|
|
172
|
+
|
|
173
|
+
replacement_endpoint = mock_endpoint
|
|
174
|
+
mock(Async::HTTP::Client) do |wrapper|
|
|
175
|
+
wrapper.wrap(:new) do |original, endpoint, **options|
|
|
176
|
+
original.call(replacement_endpoint.wrap(endpoint), **options)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
@server_task = Async do
|
|
181
|
+
mock_endpoint.run do |request|
|
|
182
|
+
Protocol::HTTP::Response[200, {}, ["Hello, World"]]
|
|
183
|
+
end
|
|
67
184
|
end
|
|
68
185
|
end
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
186
|
+
|
|
187
|
+
def after(error = nil)
|
|
188
|
+
@server_task&.stop
|
|
189
|
+
super
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "routes the request through the mock endpoint" do
|
|
193
|
+
endpoint = Async::HTTP::Endpoint.parse("https://api.example.com")
|
|
194
|
+
client = Async::HTTP::Client.new(endpoint, retries: 1)
|
|
195
|
+
response = client.get("/")
|
|
196
|
+
|
|
197
|
+
expect(response.read).to be == "Hello, World"
|
|
198
|
+
ensure
|
|
199
|
+
response&.close
|
|
200
|
+
client&.close
|
|
201
|
+
end
|
|
76
202
|
end
|
|
77
203
|
~~~
|
|
204
|
+
|
|
205
|
+
Always accept and forward `**options` when wrapping the constructor so the test does not silently change client configuration.
|
data/lib/async/http/body/pipe.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2019-
|
|
4
|
+
# Copyright, 2019-2026, by Samuel Williams.
|
|
5
5
|
# Copyright, 2020, by Bruno Sutic.
|
|
6
6
|
|
|
7
7
|
require_relative "writable"
|
|
@@ -63,7 +63,7 @@ module Async
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
# Read from the head of the pipe and write to the @output stream.
|
|
66
|
-
#
|
|
66
|
+
# A write-side close on @tail produces EOF and closes @output independently of the input direction.
|
|
67
67
|
def writer(task)
|
|
68
68
|
@writer = task
|
|
69
69
|
|
data/lib/async/http/body.rb
CHANGED
data/lib/async/http/internet.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2018-
|
|
4
|
+
# Copyright, 2018-2026, by Samuel Williams.
|
|
5
|
+
# Copyright, 2026, by Denis Talakevich.
|
|
5
6
|
|
|
6
7
|
require_relative "connection"
|
|
7
8
|
require_relative "response"
|
|
@@ -34,7 +35,11 @@ module Async
|
|
|
34
35
|
|
|
35
36
|
# Used by the client to send requests to the remote server.
|
|
36
37
|
def call(request)
|
|
37
|
-
|
|
38
|
+
# The remote peer has sent a GOAWAY frame, so it will not process any new streams on this connection. The request has not been sent yet, so it is safe to retry it on a new connection, even if it is not idempotent. This is checked first, because a connection with no streams left to drain is closed by the GOAWAY itself.
|
|
39
|
+
raise ::Protocol::HTTP::RefusedError, "Connection is going away!" if self.goaway_received?
|
|
40
|
+
|
|
41
|
+
# The connection can close after being acquired from the pool. No stream has been created yet, so the request has not been written and is safe to retry.
|
|
42
|
+
raise ::Protocol::HTTP::RefusedError, "Connection closed!" if self.closed?
|
|
38
43
|
|
|
39
44
|
response = create_response
|
|
40
45
|
write_request(response, request)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# Copyright, 2018-2026, by Samuel Williams.
|
|
5
5
|
# Copyright, 2020, by Bruno Sutic.
|
|
6
6
|
# Copyright, 2025, by Jean Boussier.
|
|
7
|
+
# Copyright, 2026, by Denis Talakevich.
|
|
7
8
|
|
|
8
9
|
require_relative "stream"
|
|
9
10
|
|
|
@@ -88,16 +89,27 @@ module Async
|
|
|
88
89
|
|
|
89
90
|
# Close the connection and stop the background reader.
|
|
90
91
|
def close(error = nil)
|
|
91
|
-
|
|
92
|
-
if @reader
|
|
93
|
-
reader = @reader
|
|
92
|
+
if reader = @reader
|
|
94
93
|
@reader = nil
|
|
95
|
-
|
|
94
|
+
|
|
95
|
+
# The reader task can close the connection itself, e.g. when the last stream completes and the connection is released back to the pool. Stopping it here would cancel the current task in the middle of this method, leaving the underlying stream open, so we let it unwind by itself: `closed?` is now true, so the read loop exits.
|
|
96
|
+
reader.stop unless reader.current?
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
super
|
|
99
100
|
end
|
|
100
101
|
|
|
102
|
+
# The connection has finished draining the streams which the remote peer accepted before its graceful GOAWAY.
|
|
103
|
+
#
|
|
104
|
+
# The last of those streams can complete on the sending side, in a task other than the background reader - the response arrived first and the request body was still being written. The reader is then parked in a blocking read and will never notice that the connection is closed, so we stop it and let its `ensure` close the connection.
|
|
105
|
+
def close_if_drained!
|
|
106
|
+
super
|
|
107
|
+
|
|
108
|
+
if self.closed? and (reader = @reader) and !reader.current?
|
|
109
|
+
reader.stop
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
101
113
|
# Start a transient background task that reads frames from the connection.
|
|
102
114
|
def read_in_background(parent: Task.current)
|
|
103
115
|
raise RuntimeError, "Connection is closed!" if closed?
|
|
@@ -142,12 +154,14 @@ module Async
|
|
|
142
154
|
|
|
143
155
|
# Can we use this connection to make requests?
|
|
144
156
|
def viable?
|
|
145
|
-
@stream&.readable?
|
|
157
|
+
!self.goaway_received? && @stream&.readable?
|
|
146
158
|
end
|
|
147
159
|
|
|
148
160
|
# @returns [Boolean] Whether the connection can be reused.
|
|
161
|
+
#
|
|
162
|
+
# Once the remote peer has sent a GOAWAY frame, it will not process any new streams on this connection, so it must not be handed out for another request, even while the streams it accepted are still being drained.
|
|
149
163
|
def reusable?
|
|
150
|
-
!self.closed?
|
|
164
|
+
!self.closed? && !self.goaway_received?
|
|
151
165
|
end
|
|
152
166
|
|
|
153
167
|
# @returns [String] The HTTP version string.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2020-
|
|
4
|
+
# Copyright, 2020-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "protocol/http/body/writable"
|
|
7
7
|
|
|
@@ -25,8 +25,8 @@ module Async
|
|
|
25
25
|
# @returns [String | Nil] The next chunk, or `nil` if the body is complete.
|
|
26
26
|
def read
|
|
27
27
|
if chunk = super
|
|
28
|
-
# If we read a chunk
|
|
29
|
-
@stream
|
|
28
|
+
# If we read a chunk from the stream, we want to extend the window if required so more data will be provided.
|
|
29
|
+
@stream&.request_window_update
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
# We track the expected length and check we got what we were expecting.
|
|
@@ -42,6 +42,17 @@ module Async
|
|
|
42
42
|
|
|
43
43
|
return chunk
|
|
44
44
|
end
|
|
45
|
+
|
|
46
|
+
# Close the application-facing input body and notify the stream that incoming data is no longer being consumed. While local output is active, the HTTP/2 stream remains open. Once output also closes, the remaining wire stream is terminated without an error.
|
|
47
|
+
# @parameter error [Exception | Nil] The error that caused the input to be closed, if any.
|
|
48
|
+
def close(error = nil)
|
|
49
|
+
super
|
|
50
|
+
|
|
51
|
+
if stream = @stream
|
|
52
|
+
@stream = nil
|
|
53
|
+
stream.finish_input(self, error)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
45
56
|
end
|
|
46
57
|
end
|
|
47
58
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2020-
|
|
4
|
+
# Copyright, 2020-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "protocol/http/body/stream"
|
|
7
7
|
|
|
@@ -54,22 +54,24 @@ module Async
|
|
|
54
54
|
# @parameter chunk [String] The data to write.
|
|
55
55
|
def write(chunk)
|
|
56
56
|
until chunk.empty?
|
|
57
|
-
|
|
57
|
+
stream = @stream or raise IOError, "HTTP/2 stream is closed!"
|
|
58
|
+
maximum_size = stream.available_frame_size
|
|
58
59
|
|
|
59
60
|
# We try to avoid synchronization if possible:
|
|
60
61
|
if maximum_size <= 0
|
|
61
62
|
@guard.synchronize do
|
|
62
|
-
maximum_size =
|
|
63
|
+
maximum_size = stream.available_frame_size
|
|
63
64
|
|
|
64
65
|
while maximum_size <= 0
|
|
65
66
|
@window_updated.wait(@guard)
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
stream = @stream or raise IOError, "HTTP/2 stream is closed!"
|
|
69
|
+
maximum_size = stream.available_frame_size
|
|
68
70
|
end
|
|
69
71
|
end
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
break unless chunk = send_data(chunk, maximum_size)
|
|
74
|
+
break unless chunk = send_data(stream, chunk, maximum_size)
|
|
73
75
|
end
|
|
74
76
|
end
|
|
75
77
|
|
|
@@ -96,6 +98,19 @@ module Async
|
|
|
96
98
|
end
|
|
97
99
|
end
|
|
98
100
|
|
|
101
|
+
# Close the wire output without cancelling a streamable body. This allows bidirectional bodies to observe an orderly input closure and finish normally. A non-streaming producer has no input side through which closure can propagate, so it is stopped directly.
|
|
102
|
+
def close_stream
|
|
103
|
+
if @body.stream?
|
|
104
|
+
@stream = nil
|
|
105
|
+
|
|
106
|
+
@guard.synchronize do
|
|
107
|
+
@window_updated.broadcast
|
|
108
|
+
end
|
|
109
|
+
else
|
|
110
|
+
stop(nil)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
99
114
|
private
|
|
100
115
|
|
|
101
116
|
def stream(task)
|
|
@@ -137,11 +152,11 @@ module Async
|
|
|
137
152
|
# @param maximum_size [Integer] send up to this many bytes of data.
|
|
138
153
|
# @param stream [Stream] the stream to use for sending data frames.
|
|
139
154
|
# @return [String, nil] any data that could not be written.
|
|
140
|
-
def send_data(chunk, maximum_size)
|
|
155
|
+
def send_data(stream, chunk, maximum_size)
|
|
141
156
|
if chunk.bytesize <= maximum_size
|
|
142
|
-
|
|
157
|
+
stream.send_data(chunk, maximum_size: maximum_size)
|
|
143
158
|
else
|
|
144
|
-
|
|
159
|
+
stream.send_data(chunk.byteslice(0, maximum_size), maximum_size: maximum_size)
|
|
145
160
|
|
|
146
161
|
# The window was not big enough to send all the data, so we save it for next time:
|
|
147
162
|
return chunk.byteslice(maximum_size, chunk.bytesize - maximum_size)
|
|
@@ -30,11 +30,13 @@ module Async
|
|
|
30
30
|
# Wait for the response headers and return the response body.
|
|
31
31
|
# @returns [Protocol::HTTP::Body::Readable | Nil] The response body.
|
|
32
32
|
def wait_for_input
|
|
33
|
+
response = @response
|
|
34
|
+
|
|
33
35
|
# The input isn't ready until the response headers have been received:
|
|
34
|
-
|
|
36
|
+
response.wait
|
|
35
37
|
|
|
36
38
|
# There is a possible race condition if you try to access @input - it might already be closed and nil.
|
|
37
|
-
return
|
|
39
|
+
return response.body
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
# Handle a push promise stream from the server.
|
|
@@ -169,6 +171,17 @@ module Async
|
|
|
169
171
|
@stream.wait
|
|
170
172
|
end
|
|
171
173
|
|
|
174
|
+
# Close this response as quickly as possible. If the response body is still active, cancel the HTTP/2 exchange rather than draining it.
|
|
175
|
+
# @parameter error [Exception | Nil] The error which closed the response.
|
|
176
|
+
def close(error = nil)
|
|
177
|
+
if @body && !@stream.closed?
|
|
178
|
+
code = error ? ::Protocol::HTTP2::Error::INTERNAL_ERROR : ::Protocol::HTTP2::Error::CANCEL
|
|
179
|
+
@stream.send_reset_stream(code)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
super
|
|
183
|
+
end
|
|
184
|
+
|
|
172
185
|
# @returns [Boolean] Whether the original request was a HEAD request.
|
|
173
186
|
def head?
|
|
174
187
|
@request&.head?
|