Skip to content

Commit 08c794c

Browse files
committed
Add more tests for persistent connection handling.
1 parent ff94412 commit 08c794c

File tree

3 files changed

+145
-2
lines changed

3 files changed

+145
-2
lines changed

lib/protocol/http1/connection.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def persistent?(version, method, headers)
148148
else
149149
return false
150150
end
151-
else
151+
else # HTTP/1.1+
152152
if connection = headers[CONNECTION]
153153
return !connection.close?
154154
else
@@ -362,7 +362,9 @@ def read_response(method)
362362

363363
headers = read_headers
364364

365-
@persistent = persistent?(version, method, headers)
365+
if @persistent
366+
@persistent = persistent?(version, method, headers)
367+
end
366368

367369
unless interim_status?(status)
368370
body = read_response_body(method, status, headers)

test/protocol/http1/connection.rb

+8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
expect(version).to be == "HTTP/1.1"
3131
expect(headers).to be == {}
3232
expect(body).to be_nil
33+
34+
expect(server).to be(:persistent)
3335
end
3436

3537
it "reads request without body after closing connection" do
@@ -44,6 +46,8 @@
4446
expect(version).to be == "HTTP/1.1"
4547
expect(headers).to be == {"accept" => ["*/*"], "header-0" => ["value 1"]}
4648
expect(body).to be_nil
49+
50+
expect(server).to be(:persistent)
4751
end
4852

4953
it "reads request with fixed body" do
@@ -58,6 +62,8 @@
5862
expect(version).to be == "HTTP/1.1"
5963
expect(headers).to be == {}
6064
expect(body.join).to be == "Hello World"
65+
66+
expect(server).to be(:persistent)
6167
end
6268

6369
it "reads request with chunked body" do
@@ -72,7 +78,9 @@
7278
expect(version).to be == "HTTP/1.1"
7379
expect(headers).to be == {}
7480
expect(body.join).to be == "Hello World"
81+
7582
expect(server).to be(:persistent?, version, method, headers)
83+
expect(server).to be(:persistent)
7684
end
7785

7886
it "reads request with CONNECT method" do
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require "protocol/http1/connection"
7+
require "protocol/http/body/buffered"
8+
require "protocol/http/body/writable"
9+
10+
require "connection_context"
11+
12+
describe Protocol::HTTP1::Connection do
13+
include_context ConnectionContext
14+
15+
with "multiple requests in a single connection" do
16+
it "handles two back-to-back GET requests (HTTP/1.1 keep-alive)" do
17+
client.write_request("localhost", "GET", "/first", "HTTP/1.1", {"Header-A" => "Value-A"})
18+
client.write_body("HTTP/1.1", nil)
19+
expect(client).to be(:half_closed_local?)
20+
21+
# Server reads it:
22+
authority, method, path, version, headers, body = server.read_request
23+
expect(authority).to be == "localhost"
24+
expect(method).to be == "GET"
25+
expect(path).to be == "/first"
26+
expect(version).to be == "HTTP/1.1"
27+
expect(headers["header-a"]).to be == ["Value-A"]
28+
expect(body).to be_nil
29+
30+
# Server writes a response:
31+
expect(server).to be(:half_closed_remote?)
32+
server.write_response("HTTP/1.1", 200, {"Res-A" => "ValA"}, "OK")
33+
server.write_body("HTTP/1.1", nil)
34+
expect(server).to be(:idle?)
35+
36+
# Client reads first response:
37+
version, status, reason, headers, body = client.read_response("GET")
38+
expect(version).to be == "HTTP/1.1"
39+
expect(status).to be == 200
40+
expect(reason).to be == "OK"
41+
expect(headers["res-a"]).to be == ["ValA"]
42+
expect(body).to be_nil
43+
44+
# Now both sides should be back to :idle (persistent re-use):
45+
expect(client).to be(:idle?)
46+
expect(server).to be(:idle?)
47+
48+
# Second request:
49+
client.write_request("localhost", "GET", "/second", "HTTP/1.1", {"Header-B" => "Value-B"})
50+
client.write_body("HTTP/1.1", nil)
51+
expect(client).to be(:half_closed_local?)
52+
53+
# Server reads it:
54+
authority, method, path, version, headers, body = server.read_request
55+
expect(authority).to be == "localhost"
56+
expect(method).to be == "GET"
57+
expect(path).to be == "/second"
58+
expect(version).to be == "HTTP/1.1"
59+
expect(headers["header-b"]).to be == ["Value-B"]
60+
expect(body).to be_nil
61+
62+
# Server writes a response:
63+
expect(server).to be(:half_closed_remote?)
64+
server.write_response("HTTP/1.1", 200, {"Res-B" => "ValB"}, "OK")
65+
server.write_body("HTTP/1.1", nil)
66+
67+
# Client reads second response:
68+
version, status, reason, headers, body = client.read_response("GET")
69+
expect(version).to be == "HTTP/1.1"
70+
expect(status).to be == 200
71+
expect(reason).to be == "OK"
72+
expect(headers["res-b"]).to be == ["ValB"]
73+
expect(body).to be_nil
74+
75+
# Confirm final states:
76+
expect(client).to be(:idle?)
77+
expect(server).to be(:idle?)
78+
end
79+
end
80+
81+
with "partial body read" do
82+
it "closes correctly if server does not consume entire fixed-length body" do
83+
# Indicate Content-Length = 11 but only read part of it on server side:
84+
client.stream.write "POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 11\r\n\r\nHello"
85+
client.stream.close
86+
87+
# Server reads request line/headers:
88+
authority, method, path, version, headers, body = server.read_request
89+
expect(method).to be == "POST"
90+
expect(body).to be_a(Protocol::HTTP1::Body::Fixed)
91+
92+
# Partially read 5 bytes only:
93+
partial = body.read
94+
expect(partial).to be == "Hello"
95+
96+
expect do
97+
body.read
98+
end.to raise_exception(EOFError)
99+
100+
# Then server forcibly closes read (simulating a deliberate stop):
101+
server.close_read
102+
103+
# Because of partial consumption, that should move the state to half-closed remote or closed, etc.
104+
expect(server).to be(:half_closed_remote?)
105+
expect(server).not.to be(:persistent)
106+
end
107+
end
108+
109+
with "no persistence" do
110+
it "closes connection after request" do
111+
server.persistent = false
112+
113+
client.write_request("localhost", "GET", "/first", "HTTP/1.1", {"Header-A" => "Value-A"})
114+
client.write_body("HTTP/1.1", nil)
115+
expect(client).to be(:half_closed_local?)
116+
117+
# Server reads it:
118+
authority, method, path, version, headers, body = server.read_request
119+
expect(authority).to be == "localhost"
120+
expect(method).to be == "GET"
121+
expect(path).to be == "/first"
122+
expect(version).to be == "HTTP/1.1"
123+
expect(headers["header-a"]).to be == ["Value-A"]
124+
expect(body).to be_nil
125+
126+
# Server writes a response:
127+
expect(server).to be(:half_closed_remote?)
128+
server.write_response("HTTP/1.1", 200, {"Res-A" => "ValA"}, "OK")
129+
server.write_body("HTTP/1.1", nil)
130+
expect(server).to be(:closed?)
131+
end
132+
end
133+
end

0 commit comments

Comments
 (0)