fix(wpt): stream the Fetch trickle fixture response

Model trickle.py by draining the request body, delaying headers and body
chunks, and writing TEST_TRICKLE lines according to ms/count. Preserve the
optional Content-Type omission and close-delimited response framing.

Exercise supported methods, defaults, duplicate/invalid parameters,
progressive delivery, and both fixed-length and chunked uploads.

Validation: 221 Python tests passed. With the same Moli binary, 16 WPT
cases add two passing cases and eight passing subtests, with no regressions.
No Rust source or build metadata changed.
This commit is contained in:
ldm0
2026-09-27 07:59:21 +08:00
parent c0a9f9abaa
commit 2a4829fa4e
3 changed files with 149 additions and 0 deletions
@@ -2345,6 +2345,9 @@ def _make_handler(
if path == "/fetch/api/resources/status.py":
self._serve_fetch_status(parsed.query, emit_body=emit_body)
return
if path == "/fetch/api/resources/trickle.py":
self._serve_fetch_trickle(parsed.query, emit_body=emit_body)
return
if path == "/fetch/api/resources/inspect-headers.py":
self._send_bytes(
"text/plain",
@@ -3363,6 +3366,41 @@ def _make_handler(
except (BrokenPipeError, ConnectionResetError):
return
def _serve_fetch_trickle(self, query: str, *, emit_body: bool) -> None:
params = parse_qs(query, keep_blank_values=True)
delay = _wpt_delay_seconds(query)
try:
count = int(params.get("count", ["50"])[0])
except ValueError:
self.send_error(500)
return
if delay is None:
self.send_error(500)
return
# Upstream reads the upload before delaying the response headers.
if not self._consume_request_body():
return
self.close_connection = True
try:
time.sleep(delay)
self.send_response(200)
if "notype" not in params:
self.send_header("Content-Type", "text/plain")
# Like wptserve's explicit writer, delimit the body by EOF.
# Flush each chunk so readers can consume it before EOF.
self.send_header("Connection", "close")
self.end_headers()
self.wfile.flush()
if not emit_body:
return
time.sleep(delay)
for _ in range(count):
self.wfile.write(b"TEST_TRICKLE\n")
self.wfile.flush()
time.sleep(delay)
except (BrokenPipeError, ConnectionResetError, OSError):
return
def _serve_fetch_status(self, query: str, *, emit_body: bool) -> None:
try:
status, text, content_type, body = _fetch_status_response(query)
+109
View File
@@ -4391,6 +4391,115 @@ test(() => {}, "ok");
finally:
connection.close()
def test_fixture_server_models_fetch_trickle_parameters_and_methods(self) -> None:
with tempfile.TemporaryDirectory() as root:
root_path = Path(root)
(root_path / "resources").mkdir()
(root_path / "resources" / "testharness.js").write_text("// testharness")
with WptFixtureServer(root_path) as server, patch(
"moli_benchmark.wpt_cross.server.time.sleep"
) as sleep:
cases = [
("", 50, "text/plain", 0.5),
("count=2&count=9&ms=1.5&ms=900", 2, "text/plain", 0.0015),
("count=0&ms=0&notype", 0, None, 0.0),
("count=-1&ms=0&notype=false", 0, None, 0.0),
]
for method in ["GET", "HEAD", "POST", "OPTIONS", "PUT", "PATCH", "DELETE", "YO"]:
for query, count, mime, delay in cases:
with self.subTest(method=method, query=query):
sleep.reset_mock()
connection = HTTPConnection("127.0.0.1", server.port, timeout=2)
try:
connection.request(
method, "/fetch/api/resources/trickle.py?" + query,
body=b"upload" if method not in {"GET", "HEAD"} else None,
)
response = connection.getresponse()
self.assertEqual(response.status, 200)
self.assertEqual(response.headers.get("Content-Type"), mime)
self.assertIsNone(response.headers.get("Content-Length"))
self.assertIsNone(response.headers.get("Transfer-Encoding"))
self.assertEqual(response.read(), b"" if method == "HEAD" else b"TEST_TRICKLE\n" * count)
expected_sleeps = 1 if method == "HEAD" else count + 2
self.assertEqual([args.args[0] for args in sleep.call_args_list], [delay] * expected_sleeps)
finally:
connection.close()
for query in ["count=invalid", "count=", "ms=invalid", "ms=-1", "ms=nan", "ms=inf"]:
with self.subTest(query=query):
connection = HTTPConnection("127.0.0.1", server.port, timeout=2)
try:
connection.request("GET", "/fetch/api/resources/trickle.py?" + query)
response = connection.getresponse()
self.assertEqual(response.status, 500)
response.read()
finally:
connection.close()
def test_fixture_server_delivers_fetch_trickle_headers_and_chunks_before_eof(self) -> None:
with tempfile.TemporaryDirectory() as root:
root_path = Path(root)
(root_path / "resources").mkdir()
(root_path / "resources" / "testharness.js").write_text("// testharness")
headers_ready, chunk_ready = threading.Event(), threading.Event()
release_first, release_second = threading.Event(), threading.Event()
sleeps = []
def pause(delay: float) -> None:
sleeps.append(delay)
if len(sleeps) == 2:
headers_ready.set()
self.assertTrue(release_first.wait(2))
elif len(sleeps) == 3:
chunk_ready.set()
self.assertTrue(release_second.wait(2))
with WptFixtureServer(root_path) as server, patch(
"moli_benchmark.wpt_cross.server.time.sleep", side_effect=pause
):
connection = HTTPConnection("127.0.0.1", server.port, timeout=2)
try:
connection.request("GET", "/fetch/api/resources/trickle.py?count=2&notype")
response = connection.getresponse()
self.assertTrue(headers_ready.wait(2))
self.assertIsNone(response.headers.get("Content-Type"))
release_first.set()
self.assertEqual(response.read(13), b"TEST_TRICKLE\n")
self.assertTrue(chunk_ready.wait(2))
release_second.set()
self.assertEqual(response.read(), b"TEST_TRICKLE\n")
self.assertEqual(sleeps, [0.5] * 4)
finally:
release_first.set()
release_second.set()
connection.close()
def test_fixture_server_reads_upload_before_starting_fetch_trickle(self) -> None:
with tempfile.TemporaryDirectory() as root:
root_path = Path(root)
(root_path / "resources").mkdir()
(root_path / "resources" / "testharness.js").write_text("// testharness")
started = threading.Event()
with WptFixtureServer(root_path) as server, patch(
"moli_benchmark.wpt_cross.server.time.sleep",
side_effect=lambda _: started.set(),
):
for framing, first, last in [
(b"Content-Length: 6", b"abc", b"def"),
(b"Transfer-Encoding: chunked", b"3\r\nabc\r\n", b"3\r\ndef\r\n0\r\n\r\n"),
]:
with self.subTest(framing=framing), socket.create_connection(("127.0.0.1", server.port), timeout=2) as connection:
started.clear()
connection.sendall(
b"POST /fetch/api/resources/trickle.py?count=1&ms=0 HTTP/1.1\r\n"
b"Host: localhost\r\n" + framing + b"\r\n\r\n" + first
)
self.assertFalse(started.wait(0.05), "response started before upload completed")
connection.sendall(last)
self.assertTrue(started.wait(2))
response = connection.makefile("rb").read()
self.assertEqual(response.split(b"\r\n\r\n", 1)[1], b"TEST_TRICKLE\n")
def test_fixture_server_models_fetch_inspect_headers_handler(self) -> None:
self.assertEqual(
_inspect_headers_response_headers(
@@ -5527,6 +5527,8 @@ event-timing/supported-types-consistent-with-self.html
fetch/api/abort/destroyed-context.html
fetch/api/basic/response-null-body.any.js?moli-wpt-any=dedicatedworker
fetch/api/basic/response-null-body.any.js?moli-wpt-any=window
fetch/api/basic/stream-response.any.js?moli-wpt-any=dedicatedworker
fetch/api/basic/stream-response.any.js?moli-wpt-any=window
fetch/api/basic/text-utf8.any.js?moli-wpt-any=dedicatedworker
fetch/api/basic/text-utf8.any.js?moli-wpt-any=window
fetch/api/cors/data-url-iframe.html