# NousResearch/hermes-agent · Enforcing request body size limits while reading webhooks

## Open now

- Nothing open on GitHub.

The three members appear to concern enforcing request body-size limits during reading: one for chunked requests in a raft-related component, and two with the same title for LINE webhooks. The titles alone do not show how closely the raft and LINE changes are related or what their status is.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #58902. Verify it addresses your case.
- **Recommended:** read PR #58902 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #54930: Enforce LINE webhook body limit while reading

## Prior & competing PRs

- **#58902: security(raft): enforce body-size limit on chunked requests**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `plugins/platforms/raft/adapter.py`, `tests/gateway/test_raft_adapter.py`
  - fix hunks:
  - `plugins/platforms/raft/adapter.py`

    ```diff
    @@ -473,7 +473,11 @@ async def connect(self, *, is_reconnect: bool = False) -> bool:
                 self._bridge_token = [redacted:secret-assignment]
                 logger.info("[raft] Auto-generated bridge token")
     
    -        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 checks below
    +        # (mirrors gateway/platforms/webhook.py's connect()).
    +        app = web.Application(client_max_size=self._max_body_bytes)
             app.router.add_get("/health", self._handle_health)
             app.router.add_post(self._path, self._handle_wake)
             app.router.add_post("/activity", self._handle_activity)
    ```

    ```diff
    @@ -600,8 +604,16 @@ async def _handle_wake(self, request: "web.Request") -> "web.Response":
     
             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({"ok": False, "error": "payload_too_large"}, status=413)
             except Exception:
                 return web.json_response({"ok": False, "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({"ok": False, "error": "payload_too_large"}, status=413)
     
             payload: Dict[str, Any] = {}
             if raw_body.strip():
    ```

    ```diff
    @@ -646,7 +658,20 @@ async def _handle_activity(self, request: "web.Request") -> "web.Response":
                 return web.json_response({"ok": False, "error": "payload_too_large"}, status=413)
     
             try:
    -            payload = json.loads(await request.text())
    +            raw_text = await request.text()
    +        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({"ok": False, "error": "payload_too_large"}, status=413)
    +        except Exception as exc:
    +            return web.json_response({"ok": False, "error": str(exc)}, status=400)
    +        if len(raw_text.encode("utf-8")) > 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({"ok": False, "error": "payload_too_large"}, status=413)
    +
    +        try:
    +            payload = json.loads(raw_text)
                 self._activity_queue.push(payload)
             except json.JSONDecodeError:
                 return web.json_response({"ok": False, "error": "invalid_json"}, status=400)
    ```
  - `tests/gateway/test_raft_adapter.py`

    ```diff
    @@ -1,5 +1,7 @@
     """Tests for the Raft channel adapter."""
     
    +import asyncio
    +import json
     import os
     from unittest.mock import AsyncMock, patch
     
    ```

    ```diff
    @@ -56,7 +58,8 @@ def _make_adapter(**extra):
     
     
     def _create_app(adapter: RaftAdapter) -> web.Application:
    -    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(adapter._path, adapter._handle_wake)
         app.router.add_post("/activity", adapter._handle_activity)
    ```

    ```diff
    @@ -407,6 +410,68 @@ def test_interrupted_turn_reports_error_stop(self):
             assert drain["events"][1]["errorClass"] == "interrupted"
     
     
    +class TestBodySize:
    +    """The wake/activity endpoints enforced max_body_bytes only via the
    +    Content-Length header; a Transfer-Encoding: chunked request
    +    (content_length=None) bypassed the cap entirely and read the full body,
    +    bounded only by aiohttp's implicit 1 MiB default. Mirrors
    +    gateway/platforms/webhook.py's TestBodySize."""
    +
    +    @pytest.mark.asyncio
    +    async def test_wake_chunked_oversized_payload_rejected(self):
    +        adapter = _make_adapter(max_body_bytes=100)
    +        adapter.set_message_handler(AsyncMock())
    +        adapter.handle_message = AsyncMock()
    +
    +        async def _chunked_body():
    +            payload = json.dumps({"eventId": "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 client:
    +            resp = await client.post(
    +                DEFAULT_PATH,
    +                data=_chunked_body(),
    +                headers={
    +                    BRIDGE_TOKEN_HEADER: "[redacted:secret-assignment]",
    +                    "Content-Type": "application/json",
    +                },
    +            )
    +            assert resp.status == 413
    +            body = await resp.json()
    +
    +        assert body == {"ok": False, "error": "payload_too_large"}
    +        adapter.handle_message.assert_not_awaited()
    +
    +    @pytest.mark.asyncio
    +    async def test_activity_chunked_oversized_payload_rejected(self):
    +        adapter = _make_adapter(max_body_bytes=100)
    +
    +        async def _chunked_body():
    +            payload = json.dumps(_activity_event("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 client:
    +            resp = await client.post(
    +                "/activity",
    +                data=_chunked_body(),
    +                headers={
    +                    BRIDGE_TOKEN_HEADER: "[redacted:secret-assignment]",
    +                    "Content-Type": "application/json",
    +                },
    +            )
    +            assert resp.status == 413
    +            body = await resp.json()
    +
    +        assert body == {"ok": False, "error": "payload_too_large"}
    +        assert adapter._activity_queue.drain()["events"] == []
    +
    +
     class TestRaftConfig:
         def test_env_enablement_auto_enables_with_raft_profile(self, monkeypatch):
             monkeypatch.setenv("RAFT_PROFILE", "my-agent")
    ```
- **#54931: Enforce LINE webhook body limit while reading**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#58902`

## Fix status

- Merged to main in PR #58902 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-05`.
- Subsystem: `gateway`
