mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
Merge pull request #58536 from NousResearch/salvage/3955-webhook-chunked-limit
fix(gateway): enforce body-size limits on chunked requests (salvage #3955 + #3949)
This commit is contained in:
commit
f23026f979
3 changed files with 51 additions and 3 deletions
|
|
@ -669,7 +669,16 @@ if AIOHTTP_AVAILABLE:
|
|||
return web.json_response(_openai_error("Request body too large.", code="body_too_large"), status=413)
|
||||
except ValueError:
|
||||
return web.json_response(_openai_error("Invalid Content-Length header.", code="invalid_content_length"), status=400)
|
||||
return await handler(request)
|
||||
try:
|
||||
return await handler(request)
|
||||
except web.HTTPRequestEntityTooLarge:
|
||||
# aiohttp's client_max_size tripped mid-read (chunked bodies carry
|
||||
# no Content-Length) — return a proper 413 instead of letting the
|
||||
# handler's broad JSON except turn it into 400 "Invalid JSON".
|
||||
return web.json_response(
|
||||
_openai_error("Request body too large.", code="body_too_large"),
|
||||
status=413,
|
||||
)
|
||||
else:
|
||||
body_limit_middleware = None # type: ignore[assignment]
|
||||
|
||||
|
|
|
|||
|
|
@ -191,7 +191,10 @@ class WebhookAdapter(BasePlatformAdapter):
|
|||
f"real target (telegram, discord, slack, github_comment, etc.)."
|
||||
)
|
||||
|
||||
app = web.Application()
|
||||
# client_max_size makes aiohttp enforce the cap on every read path,
|
||||
# including Transfer-Encoding: chunked bodies that carry no
|
||||
# Content-Length and would otherwise bypass the header check below.
|
||||
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)
|
||||
# Multi-profile multiplexing: a /p/<profile>/webhooks/<route> prefix
|
||||
|
|
@ -479,9 +482,21 @@ class WebhookAdapter(BasePlatformAdapter):
|
|||
# Read body (must be done before any validation)
|
||||
try:
|
||||
raw_body = await request.read()
|
||||
except web.HTTPRequestEntityTooLarge:
|
||||
# aiohttp's client_max_size tripped — chunked or lying
|
||||
# Content-Length. Same 413 as the header check above.
|
||||
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:
|
||||
# Defense in depth: enforce the cap on the actual bytes read even
|
||||
# if the server-level limit was bypassed or misconfigured.
|
||||
return web.json_response(
|
||||
{"error": "Payload too large"}, status=413
|
||||
)
|
||||
|
||||
# Validate HMAC signature FIRST (skip only for the explicit local-test
|
||||
# INSECURE_NO_AUTH mode). Missing/empty secrets must fail closed here,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ 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()
|
||||
# Mirror connect(): client_max_size enforces the cap on chunked bodies.
|
||||
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
|
||||
|
|
@ -741,6 +742,29 @@ class TestBodySize:
|
|||
)
|
||||
assert resp.status == 413
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chunked_oversized_payload_rejected(self):
|
||||
"""Chunked request bodies (no Content-Length) over the limit 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue