Skip to content

Commit 97382e1

Browse files
committed
Wait for input to be consumed before continuing.
1 parent f750834 commit 97382e1

File tree

4 files changed

+194
-4
lines changed

4 files changed

+194
-4
lines changed

lib/async/http/body/finishable.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2019-2023, by Samuel Williams.
5+
6+
require 'protocol/http/body/wrapper'
7+
require 'async/variable'
8+
9+
module Async
10+
module HTTP
11+
module Body
12+
class Finishable < ::Protocol::HTTP::Body::Wrapper
13+
def initialize(body)
14+
super(body)
15+
16+
@closed = Async::Variable.new
17+
@error = nil
18+
end
19+
20+
def close(error = nil)
21+
unless @closed.resolved?
22+
@error = error
23+
@closed.value = true
24+
end
25+
26+
super
27+
end
28+
29+
def wait
30+
@closed.wait
31+
end
32+
33+
def inspect
34+
"#<#{self.class} closed=#{@closed} error=#{@error}> | #{super}"
35+
end
36+
end
37+
end
38+
end
39+
end

lib/async/http/client.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ def make_response(request, connection)
188188

189189
# The connection won't be released until the body is completely read/released.
190190
::Protocol::HTTP::Body::Completable.wrap(response) do
191+
# TODO: We should probably wait until the request is fully consumed and/or the connection is ready before releasing it back into the pool.
192+
193+
# Release the connection back into the pool:
191194
@pool.release(connection)
192195
end
193196

lib/async/http/protocol/http1/server.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ def each(task: Task.current)
4646
task.annotate("Reading #{self.version} requests for #{self.class}.")
4747

4848
while request = next_request
49+
if body = request.body
50+
finishable = Body::Finishable.new(body)
51+
request.body = finishable
52+
end
53+
4954
response = yield(request, self)
5055
version = request.version
5156
body = response&.body
@@ -102,23 +107,24 @@ def each(task: Task.current)
102107
head = request.head?
103108

104109
# Same as above:
105-
request = nil unless request.body
110+
request = nil
106111
response = nil
107112

108113
write_body(version, body, head, trailer)
109114
end
110115
end
111116

112-
# We are done with the body, you shouldn't need to call close on it:
117+
# We are done with the body:
113118
body = nil
114119
else
115120
# If the request failed to generate a response, it was an internal server error:
116121
write_response(@version, 500, {})
117122
write_body(version, nil)
123+
124+
request&.finish
118125
end
119126

120-
# Gracefully finish reading the request body if it was not already done so.
121-
request&.each{}
127+
finishable&.wait
122128

123129
# This ensures we yield at least once every iteration of the loop and allow other fibers to execute.
124130
task.yield

test/async/http/protocol/streaming.rb

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2024, by Samuel Williams.
5+
6+
require "async/http/protocol/http"
7+
require "protocol/http/body/streamable"
8+
require "sus/fixtures/async/http"
9+
10+
AnEchoServer = Sus::Shared("an echo server") do
11+
let(:app) do
12+
::Protocol::HTTP::Middleware.for do |request|
13+
output = ::Protocol::HTTP::Body::Writable.new
14+
15+
Async do
16+
stream = ::Protocol::HTTP::Body::Stream.new(request.body, output)
17+
18+
Console.debug(self, "Echoing chunks...")
19+
while chunk = stream.readpartial(1024)
20+
Console.debug(self, "Reading chunk:", chunk: chunk)
21+
stream.write(chunk)
22+
end
23+
rescue EOFError
24+
Console.debug(self, "EOF.")
25+
# Ignore.
26+
ensure
27+
Console.debug(self, "Closing stream.")
28+
stream.close
29+
end
30+
31+
::Protocol::HTTP::Response[200, {}, output]
32+
end
33+
end
34+
35+
it "should echo the request body" do
36+
chunks = ["Hello,", "World!"]
37+
response_chunks = Queue.new
38+
39+
output = ::Protocol::HTTP::Body::Writable.new
40+
response = client.post("/", body: output)
41+
stream = ::Protocol::HTTP::Body::Stream.new(response.body, output)
42+
43+
begin
44+
Console.debug(self, "Echoing chunks...")
45+
chunks.each do |chunk|
46+
Console.debug(self, "Writing chunk:", chunk: chunk)
47+
stream.write(chunk)
48+
end
49+
50+
Console.debug(self, "Closing write.")
51+
stream.close_write
52+
53+
Console.debug(self, "Reading chunks...")
54+
while chunk = stream.readpartial(1024)
55+
Console.debug(self, "Reading chunk:", chunk: chunk)
56+
response_chunks << chunk
57+
end
58+
rescue EOFError
59+
Console.debug(self, "EOF.")
60+
# Ignore.
61+
ensure
62+
Console.debug(self, "Closing stream.")
63+
stream.close
64+
response_chunks.close
65+
end
66+
67+
chunks.each do |chunk|
68+
expect(response_chunks.pop).to be == chunk
69+
end
70+
end
71+
end
72+
73+
AnEchoClient = Sus::Shared("an echo client") do
74+
let(:chunks) {["Hello,", "World!"]}
75+
let(:response_chunks) {Queue.new}
76+
77+
let(:app) do
78+
::Protocol::HTTP::Middleware.for do |request|
79+
output = ::Protocol::HTTP::Body::Writable.new
80+
81+
Async do
82+
stream = ::Protocol::HTTP::Body::Stream.new(request.body, output)
83+
84+
Console.debug(self, "Echoing chunks...")
85+
chunks.each do |chunk|
86+
stream.write(chunk)
87+
end
88+
89+
Console.debug(self, "Closing write.")
90+
stream.close_write
91+
92+
Console.debug(self, "Reading chunks...")
93+
while chunk = stream.readpartial(1024)
94+
Console.debug(self, "Reading chunk:", chunk: chunk)
95+
response_chunks << chunk
96+
end
97+
rescue EOFError
98+
Console.debug(self, "EOF.")
99+
# Ignore.
100+
ensure
101+
Console.debug(self, "Closing stream.")
102+
stream.close
103+
end
104+
105+
::Protocol::HTTP::Response[200, {}, output]
106+
end
107+
end
108+
109+
it "should echo the response body" do
110+
output = ::Protocol::HTTP::Body::Writable.new
111+
response = client.post("/", body: output)
112+
stream = ::Protocol::HTTP::Body::Stream.new(response.body, output)
113+
114+
begin
115+
Console.debug(self, "Echoing chunks...")
116+
while chunk = stream.readpartial(1024)
117+
stream.write(chunk)
118+
end
119+
rescue EOFError
120+
Console.debug(self, "EOF.")
121+
# Ignore.
122+
ensure
123+
Console.debug(self, "Closing stream.")
124+
stream.close
125+
end
126+
127+
chunks.each do |chunk|
128+
expect(response_chunks.pop).to be == chunk
129+
end
130+
end
131+
end
132+
133+
[Async::HTTP::Protocol::HTTP1].each do |protocol|
134+
describe protocol do
135+
include Sus::Fixtures::Async::HTTP::ServerContext
136+
137+
let(:protocol) {subject}
138+
139+
it_behaves_like AnEchoServer
140+
it_behaves_like AnEchoClient
141+
end
142+
end

0 commit comments

Comments
 (0)