diff --git a/gateway/platforms/webhook.py b/gateway/platforms/webhook.py index 5f7c78cfaf99..8720875d68d8 100644 --- a/gateway/platforms/webhook.py +++ b/gateway/platforms/webhook.py @@ -114,7 +114,7 @@ async def connect(self) -> bool: f"For testing without auth, set secret to '{_INSECURE_NO_AUTH}'." ) - app = web.Application() + app = web.Application(client_max_size=self._max_body_bytes) app.router.add_get("/health", self._handle_health) app.router.add_post("/webhooks/{route_name}", self._handle_webhook) @@ -270,9 +270,17 @@ async def _handle_webhook(self, request: "web.Request") -> "web.Response": # Read body try: raw_body = await request.read() + except web.HTTPRequestEntityTooLarge: + return web.json_response( + {"error": "Payload too large"}, status=413 + ) except Exception as e: logger.error("[webhook] Failed to read body: %s", e) return web.json_response({"error": "Bad request"}, status=400) + if len(raw_body) > self._max_body_bytes: + return web.json_response( + {"error": "Payload too large"}, status=413 + ) # Validate HMAC signature (skip for INSECURE_NO_AUTH testing mode) secret = route_config.get("secret", self._global_secret) diff --git a/tests/gateway/test_webhook_adapter.py b/tests/gateway/test_webhook_adapter.py index 9b8a91318a9a..0ac3041f483d 100644 --- a/tests/gateway/test_webhook_adapter.py +++ b/tests/gateway/test_webhook_adapter.py @@ -67,7 +67,7 @@ def _make_adapter(routes=None, **kwargs): def _create_app(adapter: WebhookAdapter) -> web.Application: """Build the aiohttp Application from the adapter (without starting a full server).""" - app = web.Application() + app = web.Application(client_max_size=adapter._max_body_bytes) app.router.add_get("/health", adapter._handle_health) app.router.add_post("/webhooks/{route_name}", adapter._handle_webhook) return app @@ -516,6 +516,29 @@ async def test_oversized_payload_rejected(self): ) assert resp.status == 413 + @pytest.mark.asyncio + async def test_chunked_oversized_payload_rejected(self): + """Chunked request bodies over the limit still return 413.""" + routes = {"big": {"secret": _INSECURE_NO_AUTH, "prompt": "test"}} + adapter = _make_adapter(routes=routes, max_body_bytes=100) + adapter.handle_message = AsyncMock() + + async def _chunked_body(): + payload = json.dumps({"data": "x" * 500}).encode("utf-8") + for i in range(0, len(payload), 64): + yield payload[i : i + 64] + await asyncio.sleep(0) + + app = _create_app(adapter) + async with TestClient(TestServer(app)) as cli: + resp = await cli.post( + "/webhooks/big", + data=_chunked_body(), + headers={"Content-Type": "application/json"}, + ) + assert resp.status == 413 + adapter.handle_message.assert_not_awaited() + # =================================================================== # INSECURE_NO_AUTH