Skip to content

Commit 7f1c442

Browse files
committed
add localhost test for the corner case
1 parent a9745ea commit 7f1c442

4 files changed

Lines changed: 248 additions & 32 deletions

File tree

.builder/action/local-server-setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,4 @@ def run(self, env):
4949
def close_local_server():
5050
p_server.terminate()
5151
p_non_tls_server.terminate()
52+
p_h11_server.terminate()

tests/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,9 @@ if(ENABLE_LOCALHOST_INTEGRATION_TESTS)
292292
add_net_test_case(localhost_integ_hpack_compression_stress)
293293
add_net_test_case(localhost_integ_h2_upload_stress)
294294
add_net_test_case(localhost_integ_h2_download_stress)
295+
# Test for H1 client handling response without Content-Length header
296+
# Requires h11mock_server.py to be running on localhost:8081
297+
add_net_test_case(localhost_integ_h1_no_content_length_response)
295298
endif()
296299

297300
add_test_case(h2_header_empty_payload)

tests/mock_server/h11mock_server.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async def _read_from_peer(self):
4242
request_line_end = data.index(b'\r\n')
4343
request_line = data[:request_line_end]
4444
rest = data[request_line_end:]
45-
45+
4646
try:
4747
# Try to decode and check if there are non-ASCII chars
4848
request_line_str = request_line.decode('ascii')
@@ -66,7 +66,7 @@ async def _read_from_peer(self):
6666
else:
6767
encoded_query_parts.append(param)
6868
target_str = path + '?' + '&'.join(encoded_query_parts)
69-
69+
7070
request_line = b' '.join([method, target_str.encode('ascii'), version])
7171
data = request_line + rest
7272
except (ConnectionError, trio.BrokenResourceError, trio.ClosedResourceError):
@@ -87,7 +87,7 @@ async def shutdown_and_clean_up(self):
8787
await self.stream.send_eof()
8888
except (trio.BrokenResourceError, AttributeError):
8989
pass
90-
90+
9191
with trio.move_on_after(TIMEOUT):
9292
try:
9393
while True:
@@ -96,7 +96,7 @@ async def shutdown_and_clean_up(self):
9696
break
9797
except (trio.BrokenResourceError, trio.ClosedResourceError):
9898
pass
99-
99+
100100
try:
101101
await self.stream.aclose()
102102
except (trio.BrokenResourceError, trio.ClosedResourceError):
@@ -177,36 +177,59 @@ async def maybe_send_error_response(wrapper, exc):
177177

178178
async def send_echo_response(wrapper, request):
179179
wrapper.info("Preparing echo response")
180-
180+
181181
body_data = b""
182182
while True:
183183
event = await wrapper.next_event()
184184
if type(event) is h11.EndOfMessage:
185185
break
186186
assert type(event) is h11.Data
187187
body_data += event.data
188-
188+
189189
target = request.target if isinstance(request.target, bytes) else request.target.encode()
190190
target_str = target.decode("utf-8", errors="replace")
191-
191+
192+
# Check if this is the /no-content-length endpoint
193+
if target_str.startswith("/no-content-length"):
194+
wrapper.info("Sending raw response without Content-Length or Transfer-Encoding")
195+
response_body = b"Response body without Content-Length header"
196+
197+
# Send raw HTTP response to avoid h11 adding Transfer-Encoding: chunked
198+
# Build the raw HTTP response
199+
response_lines = [
200+
b"HTTP/1.1 200 OK",
201+
b"Server: echo-server",
202+
b"Content-Type: text/plain; charset=utf-8",
203+
b"Connection: close",
204+
b"", # Empty line separates headers from body
205+
]
206+
raw_response = b"\r\n".join(response_lines) + b"\r\n" + response_body
207+
208+
# Send raw response directly to the stream, bypassing h11
209+
await wrapper.stream.send_all(raw_response)
210+
211+
# Mark connection as must close since we sent raw data
212+
wrapper.conn.send_failed() # This marks the connection as broken in h11's view
213+
return
214+
192215
# Check if this is the /404 endpoint
193216
if target_str.startswith("/404"):
194217
status_code = 404
195218
else:
196219
status_code = 200
197-
220+
198221
response_json = {"data": body_data.decode("utf-8")}
199222
response_body = json.dumps(response_json, indent=4).encode("utf-8")
200-
223+
201224
headers = wrapper.basic_headers()
202225
headers.append(("Content-Type", "application/json; charset=utf-8"))
203226
headers.append(("Content-Length", str(len(response_body))))
204-
227+
205228
for header_name, header_value in request.headers:
206229
echo_name = b"Echo-" + header_name if isinstance(header_name, bytes) else f"Echo-{header_name}".encode()
207230
echo_value = header_value if isinstance(header_value, bytes) else str(header_value).encode()
208231
headers.append((echo_name, echo_value))
209-
232+
210233
res = h11.Response(status_code=status_code, headers=headers)
211234
await wrapper.send(res)
212235
await wrapper.send(h11.Data(data=response_body))
@@ -225,20 +248,20 @@ async def serve_ssl(port, cert_file=os.path.join(os.path.dirname(__file__),
225248
"../resources/unittests.crt"), key_file=os.path.join(os.path.dirname(__file__),
226249
"../resources/unittests.key")):
227250
import ssl
228-
251+
229252
script_dir = os.path.dirname(os.path.abspath(__file__))
230253
cert_path = os.path.join(script_dir, cert_file)
231254
key_path = os.path.join(script_dir, key_file)
232-
255+
233256
if not os.path.exists(cert_path) or not os.path.exists(key_path):
234257
print(f"Warning: SSL certificates not found at {cert_path} and {key_path}")
235258
print(f"Skipping HTTPS server on port {port}")
236259
print(f"To enable HTTPS, run: openssl req -x509 -newkey rsa:2048 -keyout {key_file} -out {cert_file} -days 365 -nodes -subj '/C=US/ST=WA/L=Seattle/O=Test/CN=localhost'")
237260
return
238-
261+
239262
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
240263
ssl_context.load_cert_chain(cert_path, key_path)
241-
264+
242265
print(f"listening on https://localhost:{port}")
243266
try:
244267
await trio.serve_ssl_over_tcp(http_serve, port, ssl_context)
@@ -249,7 +272,7 @@ async def serve_ssl(port, cert_file=os.path.join(os.path.dirname(__file__),
249272
async def main():
250273
http_port = os.environ.get('HTTP_PORT')
251274
https_port = os.environ.get('HTTPS_PORT')
252-
275+
253276
async with trio.open_nursery() as nursery:
254277
nursery.start_soon(serve, 8081 if not http_port else int(http_port))
255278
nursery.start_soon(serve_ssl, 8082 if not https_port else int(https_port))

0 commit comments

Comments
 (0)