From 4e375fc8a97a4dfb2eb82cadfef86bcd6f36da82 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 12 Sep 2026 08:55:45 +0800 Subject: [PATCH] fix(wpt): model XHR network error fixtures Serve the upstream malformed chunk sequence and infinite redirect loop instead of returning Python source. Preserve redirect parameters and origin, explicit raw body writes, and supported endpoint filtering. Validation: all 550 benchmark tests pass, including raw wire and partial HTTP body decoding regressions. With the same Moli binary, 88 focused WPT cases improve from 77 to 82 passes (127 to 132 of 143 subtests). The corrected fixture also exposes a page XHR bug: malformed-body handling now times out instead of failing on unexpected load. Its streaming failure branch records the network error without dispatching XHR error/loadend; that engine fix follows separately. The worker variant now passes. --- .../moli_benchmark/wpt_cross/case_set.py | 1 + .../moli_benchmark/wpt_cross/server.py | 76 +++++++-- .../test_wpt_xhr_network_error_server.py | 153 ++++++++++++++++++ 3 files changed, 219 insertions(+), 11 deletions(-) create mode 100644 moli-benchmark/tests/test_wpt_xhr_network_error_server.py diff --git a/moli-benchmark/moli_benchmark/wpt_cross/case_set.py b/moli-benchmark/moli_benchmark/wpt_cross/case_set.py index ff687c79a3..a9bbcd6b6d 100644 --- a/moli-benchmark/moli_benchmark/wpt_cross/case_set.py +++ b/moli-benchmark/moli_benchmark/wpt_cross/case_set.py @@ -723,6 +723,7 @@ def _xhr_handler_reference_patterns(directory: str) -> tuple[re.Pattern[str], .. "echo-content-type.py", "corsenabled.py", "win-1252-xml.py", "win-1252-html.py", "invalid-utf8-html.py", "shift-jis-html.py", "img-utf8-html.py", "empty-div-utf8-html.py", "status.py", "last-modified.py", + "bad-chunk-encoding.py", "infinite-redirects.py", ): resource = f"xhr/resources/{name}" relative = posixpath.relpath(resource, directory) diff --git a/moli-benchmark/moli_benchmark/wpt_cross/server.py b/moli-benchmark/moli_benchmark/wpt_cross/server.py index a071ba4a1f..80b0a200fa 100644 --- a/moli-benchmark/moli_benchmark/wpt_cross/server.py +++ b/moli-benchmark/moli_benchmark/wpt_cross/server.py @@ -89,6 +89,8 @@ XHR_RESOURCE_PATHS = { "/xhr/resources/echo-content-type.py", "/xhr/resources/status.py", "/xhr/resources/last-modified.py", + "/xhr/resources/bad-chunk-encoding.py", + "/xhr/resources/infinite-redirects.py", *XHR_DOCUMENT_FIXTURES, } FETCH_ABORT_RESOURCE_PATHS = { @@ -2676,21 +2678,33 @@ def _make_handler( path = unquote(parsed.path) if path not in XHR_RESOURCE_PATHS: return False + if path == "/xhr/resources/bad-chunk-encoding.py": + self._serve_xhr_bad_chunk_encoding() + return True upload_consumed = False + cache_control = "no-store" try: if path == "/xhr/resources/requri.py": params = parse_qs(parsed.query, keep_blank_values=True) - uri = self.path - if "full" in params and not uri.startswith("http://"): - authority = self.headers.get("Host") - if authority is None: - authority = _url_host_literal( - str(self.server.server_address[0]) - ) - if urlsplit("//" + authority).port is None: - authority += ":" + str(self.server.server_address[1]) - uri = f"http://{authority}{uri}" + uri = self._xhr_request_url() if "full" in params else self.path status, reason, headers, body = 200, None, [], uri.encode("utf-8") + elif path == "/xhr/resources/infinite-redirects.py": + params = parse_qs(parsed.query, keep_blank_values=True, encoding="latin-1") + page = "default" if params.get("page", [None])[0] == "alternate" else "alternate" + redirect_type = 301 if params.get("type", [None])[0] == "301" else 302 + mix = int(params.get("mix", [None])[0] == "1") + if mix: + redirect_type = 302 if redirect_type == 301 else 301 + request_url = urlsplit(self._xhr_request_url()) + location = urlunsplit(( + request_url.scheme, request_url.netloc, request_url.path, + f"page={page}&type={redirect_type}&mix={mix}", "", + )) + # Upstream returns 301 regardless of the next URL's `type`. + status, reason = 301, None + headers = [("Pragma", "no-cache"), ("Location", location)] + cache_control = "no-cache" + body = ("Hello guest. You have been redirected to " + location).encode("utf-8") elif path == "/xhr/resources/inspect-headers.py": status, reason = 200, None headers, body = _xhr_inspect_headers_fixture_response( @@ -2797,9 +2811,48 @@ def _make_handler( extra_headers=headers, status_code=status, status_text=reason, + cache_control=cache_control, ) return True + def _xhr_request_url(self) -> str: + if self.path.startswith("http://"): + return self.path + authority = self.headers.get("Host") + if authority is None: + authority = _url_host_literal(str(self.server.server_address[0])) + if urlsplit("//" + authority).port is None: + authority += ":" + str(self.server.server_address[1]) + return f"http://{authority}{self.path}" + + def _serve_xhr_bad_chunk_encoding(self) -> None: + # The upstream explicit writer sends these bytes even for HEAD. + # Use raw framing so clients receive data before a decoding error. + self.close_connection = True + self.protocol_version = self.request_version + try: + if stopping.wait(0.1): + return + self.send_response(200) + self.send_header("Transfer-Encoding", "chunked") + self.send_header("Content-Type", "text/plain") + self.send_header("X-Content-Type-Options", "nosniff") + self.send_header("Connection", "close") + self.end_headers() + self.wfile.flush() + if stopping.wait(0.1): + return + for _ in range(5): + self.wfile.write(b"a\r\nTEST_CHUNK\r\n") + self.wfile.flush() + if stopping.wait(0.1): + return + self.wfile.write(b"garbage") + self.wfile.flush() + except OSError: + # Clients may abort once they receive a partial response. + return + def _serve_xhr_delay(self, query: str, *, emit_body: bool) -> None: delay_seconds = _wpt_delay_seconds(query) if delay_seconds is None: @@ -3003,6 +3056,7 @@ def _make_handler( extra_headers: list[tuple[str, str]] | None = None, status_code: int = 200, status_text: str | None = None, + cache_control: str = "no-store", ) -> None: if content_type is None: header_block = list(extra_headers or []) @@ -3017,7 +3071,7 @@ def _make_handler( self.send_header(name, value) if not _headers_include(header_block, "Content-Length"): self.send_header("Content-Length", str(len(body))) - self.send_header("Cache-Control", "no-store") + self.send_header("Cache-Control", cache_control) self.end_headers() if emit_body: declared_length = next( diff --git a/moli-benchmark/tests/test_wpt_xhr_network_error_server.py b/moli-benchmark/tests/test_wpt_xhr_network_error_server.py new file mode 100644 index 0000000000..f90b2ddaaa --- /dev/null +++ b/moli-benchmark/tests/test_wpt_xhr_network_error_server.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import socket +import tempfile +import unittest +from contextlib import ExitStack +from http.client import HTTPConnection, IncompleteRead +from pathlib import Path +from unittest.mock import patch +from urllib.parse import urlsplit + +from moli_benchmark.wpt_cross.case_set import enumerate_cases +from moli_benchmark.wpt_cross.server import WptFixtureServer + + +RESOURCE_DIR = "/xhr/resources/" + + +class XhrNetworkErrorFixtureTests(unittest.TestCase): + def setUp(self) -> None: + self.stack = ExitStack() + self.addCleanup(self.stack.close) + self.root = Path(self.stack.enter_context(tempfile.TemporaryDirectory())) + (self.root / "resources").mkdir() + (self.root / "resources/testharness.js").write_text("// testharness") + (self.root / "xhr/resources").mkdir(parents=True) + for name in ("bad-chunk-encoding.py", "infinite-redirects.py"): + (self.root / "xhr/resources" / name).write_text("# must not serve Python source") + self.stack.enter_context(patch( + "moli_benchmark.wpt_cross.server._global_ipv6_address", return_value=None + )) + + def server(self) -> WptFixtureServer: + return self.stack.enter_context(WptFixtureServer(self.root)) + + def test_bad_chunk_response_preserves_wire_framing_and_explicit_head_writes(self) -> None: + server = self.server() + for method in ("GET", "HEAD", "CHICKEN"): + with self.subTest(method=method), socket.create_connection( + ("127.0.0.1", server.port), timeout=3 + ) as connection: + connection.sendall(( + f"{method} {RESOURCE_DIR}bad-chunk-encoding.py HTTP/1.1\r\n" + f"Host: localhost:{server.port}\r\n\r\n" + ).encode()) + with connection.makefile("rb") as response: + headers, body = response.read().split(b"\r\n\r\n", 1) + self.assertTrue(headers.startswith(b"HTTP/1.1 200 ")) + fields = headers.lower().split(b"\r\n")[1:] + for field in ( + b"transfer-encoding: chunked", b"content-type: text/plain", + b"x-content-type-options: nosniff", b"connection: close", + ): + self.assertIn(field, fields) + self.assertFalse(any(field.startswith(b"content-length:") for field in fields)) + self.assertEqual(body, b"a\r\nTEST_CHUNK\r\n" * 5 + b"garbage") + + def test_http_client_can_read_partial_body_before_chunk_decoding_fails(self) -> None: + server = self.server() + connection = HTTPConnection("127.0.0.1", server.port, timeout=3) + self.addCleanup(connection.close) + # This upstream handler never reads the upload, even when it is unfinished. + connection.putrequest("POST", RESOURCE_DIR + "bad-chunk-encoding.py") + connection.putheader("Content-Length", "1000000") + connection.endheaders() + response = connection.getresponse() + self.assertEqual(response.status, 200) + self.assertTrue(response.chunked) + self.assertEqual(response.read(10), b"TEST_CHUNK") + with self.assertRaises(IncompleteRead) as failure: + response.read() + self.assertEqual(failure.exception.partial, b"TEST_CHUNK" * 4) + + def redirect( + self, port: int, target: str, *, method: str = "GET", host: str | None = None, + ) -> tuple[int, dict[str, str], bytes]: + connection = HTTPConnection("127.0.0.1", port, timeout=3) + try: + connection.request(method, target, headers={"Host": host} if host else {}) + response = connection.getresponse() + return response.status, dict(response.getheaders()), response.read() + finally: + connection.close() + + def test_infinite_redirect_query_parameters_and_status_match_upstream(self) -> None: + server = self.server() + base = f"http://localhost:{server.port}" + RESOURCE_DIR + "infinite-redirects.py" + cases = [ + ("", "alternate", 302, 0), + ("page=alternate", "default", 302, 0), + ("page=alternate&page=default&type=301&type=302&mix=1&mix=0", "default", 302, 1), + ("page=default&page=alternate&type=301&mix=0", "alternate", 301, 0), + ("type=other&mix=1", "alternate", 301, 1), + ("page=&type=&mix=", "alternate", 302, 0), + ] + for query, page, redirect_type, mix in cases: + with self.subTest(query=query): + status, headers, body = self.redirect(server.port, base + "?" + query) + expected = f"{base}?page={page}&type={redirect_type}&mix={mix}" + # Upstream always returns 301; `type` only controls the next URL. + self.assertEqual(status, 301) + self.assertEqual(headers["Location"], expected) + self.assertIn("no-cache", headers["Cache-Control"]) + self.assertEqual(headers["Pragma"], "no-cache") + self.assertEqual(body, ("Hello guest. You have been redirected to " + expected).encode()) + + def test_infinite_redirects_keep_looping_on_the_requested_origin(self) -> None: + server = self.server() + for port, host in ((server.port, "www1.localhost"), + (server.alternate_port, f"www2.localhost:{server.alternate_port}")): + for method in ("GET", "HEAD", "OPTIONS", "CHICKEN"): + with self.subTest(port=port, host=host, method=method): + target = RESOURCE_DIR + "%69nfinite-redirects.py?mix=1" + authority = host if ":" in host else f"{host}:{port}" + for hop in range(6): + status, headers, body = self.redirect(port, target, method=method, host=host) + self.assertEqual(status, 301) + next_url = urlsplit(headers["Location"]) + self.assertEqual((next_url.scheme, next_url.netloc), ("http", authority)) + self.assertEqual(next_url.path, RESOURCE_DIR + "%69nfinite-redirects.py") + self.assertEqual(next_url.query, ( + "page=alternate&type=301&mix=1" if hop % 2 == 0 + else "page=default&type=302&mix=1" + )) + expected_body = ("Hello guest. You have been redirected to " + headers["Location"]).encode() + self.assertEqual(headers["Content-Length"], str(len(expected_body))) + self.assertEqual(body, b"" if method == "HEAD" else expected_body) + target = headers["Location"] + + def test_case_selection_accepts_only_the_supported_error_fixture_paths(self) -> None: + sources = { + "xhr/error.any.js": "fetch('resources/bad-chunk-encoding.py');", + "xhr/loop.html": "fetch('/xhr/resources/infinite-redirects.py');", + "xhr/nested/both.html": "fetch('../resources/infinite-redirects.py'); fetch('../resources/bad-chunk-encoding.py');", + "xhr/unknown.html": "fetch('resources/infinite-redirects.py'); fetch('resources/unknown.py');", + "xhr/suffix.html": "fetch('resources/bad-chunk-encoding.py-extra');", + "xhr/prefix.html": "fetch('/wrong/xhr/resources/infinite-redirects.py');", + "xhr/wrong-parent.html": "fetch('../resources/bad-chunk-encoding.py');", + "xhr/fetch-handler.html": "fetch('/fetch/api/resources/bad-chunk-encoding.py');", + } + for path, source in sources.items(): + target = self.root / path + target.parent.mkdir(parents=True, exist_ok=True) + if path.endswith(".html"): + source = '" + target.write_text(source) + self.assertEqual([case.case_path for case in enumerate_cases( + self.root, dir_prefixes=("xhr",), any_js_global="both", + )], [ + "xhr/error.any.js?moli-wpt-any=dedicatedworker", + "xhr/error.any.js?moli-wpt-any=window", + "xhr/loop.html", "xhr/nested/both.html", + ])