From a878ac7f144390cf8c4affbcfe31a2d6c2e2707f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 01:34:46 +0000 Subject: [PATCH 1/2] Test the body cap through Default.fetch instead of grepping main.py The bridge-side cap is already exercised behaviourally by test_process_request_rejects_oversize_body_before_running_app. The only contract unique to the source-text test was that main.py hands the cap to the bridge; the grep passed as long as any one of the three asgi.fetch call sites still had it. Drive all three (cached GET miss, uncacheable GET, POST) through Default.fetch and assert each passes max_body_bytes=100_000, the limit the user-facing "over 100 kB" message promises. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019boFsNWgtPe2zZQxPFRRWp --- tests/test_main_observability.py | 51 ++++++++++++++++++++++++++ tests/test_worker_asgi_bridge_scope.py | 11 ------ 2 files changed, 51 insertions(+), 11 deletions(-) diff --git a/tests/test_main_observability.py b/tests/test_main_observability.py index 3b39a59..61cd30b 100644 --- a/tests/test_main_observability.py +++ b/tests/test_main_observability.py @@ -175,6 +175,57 @@ async def asgi_fetch(*args, **kwargs): self.assertEqual(emitted[-1]["status_code"], 413) self.assertEqual(emitted[-1]["outcome"], "client_error") + def test_every_bridge_call_caps_the_request_body_at_the_submit_limit(self): + # Covers the three asgi.fetch call sites: cached GET miss, uncacheable + # GET, and POST. Each must hand the bridge the pre-ASGI body cap. + main = self.import_main() + bridge_kwargs = [] + + class FakeCache: + async def match(self, key): + return None + + async def put(self, key, value): + pass + + class FakeJsRequest: + @staticmethod + def new(url, init=None): + return url + + async def asgi_fetch(*args, **kwargs): + bridge_kwargs.append(kwargs) + response = SimpleNamespace(status=200, headers=_Headers()) + response.clone = lambda: response + return response + + main.caches = SimpleNamespace(default=FakeCache()) + main.JsRequest = FakeJsRequest + main.asgi.fetch = asgi_fetch + main.observability.emit = lambda value, env=None: None + worker = main.Default() + worker.env = SimpleNamespace() + + for method, path, cache in [ + ("GET", "/examples/values", "miss"), + ("GET", "/layout-options/a", "bypass"), + ("POST", "/examples/values", "bypass"), + ]: + with self.subTest(method=method, path=path): + event = {"cache": "bypass"} + main.observability.event_from_worker_request = ( + lambda *args, event=event, **kwargs: event + ) + request = SimpleNamespace( + method=method, + url=f"https://www.pythonbyexample.dev{path}", + js_object=SimpleNamespace(), + ) + asyncio.run(worker.fetch(request)) + self.assertEqual(event["cache"], cache) + self.assertEqual(bridge_kwargs[-1].get("max_body_bytes"), 100_000) + self.assertEqual(len(bridge_kwargs), 3) + def test_dynamic_response_reader_cancels_stream_above_output_cap(self): main = self.import_main() cancelled = [] diff --git a/tests/test_worker_asgi_bridge_scope.py b/tests/test_worker_asgi_bridge_scope.py index df3451a..d56e578 100644 --- a/tests/test_worker_asgi_bridge_scope.py +++ b/tests/test_worker_asgi_bridge_scope.py @@ -1,6 +1,5 @@ import asyncio import importlib -import pathlib import sys import types import unittest @@ -8,8 +7,6 @@ from typing import ClassVar from urllib.parse import urlparse -ROOT = pathlib.Path(__file__).resolve().parents[1] - class WorkerAsgiBridgeScopeTests(unittest.TestCase): def setUp(self): @@ -347,14 +344,6 @@ async def scenario(): asyncio.run(scenario()) self.assertEqual(calls, {"startup": 1, "request": 2}) - def test_bridge_has_pre_asgi_body_cap_hook(self): - bridge_source = (ROOT / "src" / "worker_asgi_bridge.py").read_text() - main_source = (ROOT / "src" / "main.py").read_text() - - self.assertIn("max_body_bytes", bridge_source) - self.assertIn("body_bytes > max_body_bytes", bridge_source) - self.assertIn("max_body_bytes=MAX_SUBMITTED_BODY_BYTES", main_source) - if __name__ == "__main__": unittest.main() From b68843edd1b24d097e865240187c8180b5025442 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 01:36:21 +0000 Subject: [PATCH 2/2] Assert the bridge cap agrees with the submit limit and is forwarded by bridge.fetch Review follow-ups: compare against MAX_SUBMITTED_BODY_BYTES rather than a literal (the contract is that the two caps agree), check the call count per subtest so a skipped call site cannot reuse the previous call's kwargs, and cover worker_asgi_bridge.fetch forwarding max_body_bytes to process_request, which no test (old or new) exercised. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019boFsNWgtPe2zZQxPFRRWp --- tests/test_main_observability.py | 22 ++++++++++++++-------- tests/test_worker_asgi_bridge_scope.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/tests/test_main_observability.py b/tests/test_main_observability.py index 61cd30b..27f0434 100644 --- a/tests/test_main_observability.py +++ b/tests/test_main_observability.py @@ -177,7 +177,8 @@ async def asgi_fetch(*args, **kwargs): def test_every_bridge_call_caps_the_request_body_at_the_submit_limit(self): # Covers the three asgi.fetch call sites: cached GET miss, uncacheable - # GET, and POST. Each must hand the bridge the pre-ASGI body cap. + # GET, and POST. Each must hand the bridge the same cap run_example + # enforces, or the friendly "too large" page becomes unreachable. main = self.import_main() bridge_kwargs = [] @@ -206,11 +207,14 @@ async def asgi_fetch(*args, **kwargs): worker = main.Default() worker.env = SimpleNamespace() - for method, path, cache in [ - ("GET", "/examples/values", "miss"), - ("GET", "/layout-options/a", "bypass"), - ("POST", "/examples/values", "bypass"), - ]: + for calls, (method, path, cache) in enumerate( + [ + ("GET", "/examples/values", "miss"), + ("GET", "/layout-options/a", "bypass"), + ("POST", "/examples/values", "bypass"), + ], + start=1, + ): with self.subTest(method=method, path=path): event = {"cache": "bypass"} main.observability.event_from_worker_request = ( @@ -223,8 +227,10 @@ async def asgi_fetch(*args, **kwargs): ) asyncio.run(worker.fetch(request)) self.assertEqual(event["cache"], cache) - self.assertEqual(bridge_kwargs[-1].get("max_body_bytes"), 100_000) - self.assertEqual(len(bridge_kwargs), 3) + self.assertEqual(len(bridge_kwargs), calls) + self.assertEqual( + bridge_kwargs[-1].get("max_body_bytes"), main.MAX_SUBMITTED_BODY_BYTES + ) def test_dynamic_response_reader_cancels_stream_above_output_cap(self): main = self.import_main() diff --git a/tests/test_worker_asgi_bridge_scope.py b/tests/test_worker_asgi_bridge_scope.py index d56e578..d3f3ad7 100644 --- a/tests/test_worker_asgi_bridge_scope.py +++ b/tests/test_worker_asgi_bridge_scope.py @@ -240,6 +240,26 @@ async def app(scope, receive, send): # pragma: no cover - must not run self.assertEqual(response.status, 413) self.assertFalse(ran["app"], "oversize body must be rejected before the app runs") + def test_fetch_forwards_body_cap_to_request_processing(self): + ran = {"app": False} + + async def app(scope, receive, send): # pragma: no cover - must not run + ran["app"] = True + + async def start_application(_app): + pass + + self.bridge._app_lifespans.clear() + self.bridge.start_application = start_application + req = self._fake_request( + url="https://x.dev/examples/values", + headers={"content-type": "text/plain"}, + body_chunks=[b"toolong"], + ) + response = asyncio.run(self.bridge.fetch(app, req, SimpleNamespace(), max_body_bytes=3)) + self.assertEqual(response.status, 413) + self.assertFalse(ran["app"], "fetch must apply the cap before the app runs") + def test_request_to_scope_accepts_state_without_request_globals_or_extra_scope(self): class FakeRequest(self.Request): method = "POST"