hermes - ✅(Solved) Fix webhook: max_body_bytes check is bypassed for chunked requests without Content-Length [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#12543Fetched 2026-04-20 12:18:25
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Participants

The webhook adapter enforces max_body_bytes only from request.content_length. Chunked requests with no Content-Length header skip the size check entirely, and _handle_webhook() still reads the full body into memory.

Root Cause

The webhook adapter enforces max_body_bytes only from request.content_length. Chunked requests with no Content-Length header skip the size check entirely, and _handle_webhook() still reads the full body into memory.

PR fix notes

PR #13336: fix(webhook): cap chunked request bodies

Description (problem / solution / changelog)

Fixes #12543.

Root cause: The webhook adapter only enforced max_body_bytes from request.content_length. Chunked requests without Content-Length were treated as length zero, then request.read() pulled the full body into memory before validation.

Fix summary:

  • Add limited body reading for webhook requests.
  • Keep the existing early Content-Length rejection for known oversized bodies.
  • Stream no-length/chunked bodies with request.content.iter_chunked() and return 413 as soon as the accumulated body exceeds max_body_bytes.
  • Keep a fallback post-read length check for request objects without a stream interface.
  • Add a regression proving an oversized no-length chunked request returns 413 without calling request.read() or dispatching the webhook.

Tests:

  • uv run --frozen --python 3.11 --extra dev pytest -o addopts= tests/gateway/test_webhook_adapter.py -q -> 37 passed
  • git diff --check -- gateway/platforms/webhook.py tests/gateway/test_webhook_adapter.py

Changed files

  • gateway/platforms/webhook.py (modified, +34/-7)
  • tests/gateway/test_webhook_adapter.py (modified, +41/-0)

Code Example

from gateway.platforms.webhook import WebhookAdapter
from gateway.config import PlatformConfig

class FakeRequest:
    def __init__(self, body):
        self.match_info = {'route_name': 'demo'}
        self.headers = {'Content-Type': 'application/json'}
        self.content_length = None
        self.method = 'POST'
        self._body = body
        self.read_called = False
    async def read(self):
        self.read_called = True
        return self._body

adapter = WebhookAdapter(PlatformConfig(enabled=True, extra={
    'routes': {'demo': {'source': 'unit'}},
    'max_body_bytes': 100,
}))
adapter.handle_message = lambda event: asyncio.sleep(0)
adapter._background_tasks = set()

req = FakeRequest(b'{"data":"' + b'x'*500 + b'"}')
resp = asyncio.run(adapter._handle_webhook(req))
print(resp.status, req.read_called)
# Actual: 202 True
# Expected: 413 False
RAW_BUFFERClick to expand / collapse

Summary

The webhook adapter enforces max_body_bytes only from request.content_length. Chunked requests with no Content-Length header skip the size check entirely, and _handle_webhook() still reads the full body into memory.

Affected code

  • gateway/platforms/webhook.py:290-310
  • tests/gateway/test_webhook_adapter.py:503-517 (covers oversized Content-Length, but not chunked/no-length bodies)

Why this is a bug

content_length = request.content_length or 0 treats unknown length as zero. A chunked upload therefore bypasses the 413 Payload too large guard even when the actual payload exceeds max_body_bytes.

Minimal reproduction

from gateway.platforms.webhook import WebhookAdapter
from gateway.config import PlatformConfig

class FakeRequest:
    def __init__(self, body):
        self.match_info = {'route_name': 'demo'}
        self.headers = {'Content-Type': 'application/json'}
        self.content_length = None
        self.method = 'POST'
        self._body = body
        self.read_called = False
    async def read(self):
        self.read_called = True
        return self._body

adapter = WebhookAdapter(PlatformConfig(enabled=True, extra={
    'routes': {'demo': {'source': 'unit'}},
    'max_body_bytes': 100,
}))
adapter.handle_message = lambda event: asyncio.sleep(0)
adapter._background_tasks = set()

req = FakeRequest(b'{"data":"' + b'x'*500 + b'"}')
resp = asyncio.run(adapter._handle_webhook(req))
print(resp.status, req.read_called)
# Actual: 202 True
# Expected: 413 False

Expected behavior

  • Requests larger than max_body_bytes should be rejected before the full body is read, regardless of whether Content-Length is known.

Actual behavior

  • Oversized chunked requests are fully read and accepted into the normal processing path.

Suggested investigation

  • Enforce a streaming read limit when Content-Length is missing.
  • Add a regression test for chunked/no-length bodies.

extent analysis

TL;DR

Enforce a streaming read limit when Content-Length is missing to prevent oversized chunked requests from being fully read into memory.

Guidance

  • Modify the _handle_webhook() function to check for the presence of Content-Length and enforce a streaming read limit if it's missing.
  • Add a check to ensure that the read limit is enforced before the full body is read, to prevent oversized requests from being accepted.
  • Implement a mechanism to track the total bytes read and compare it to max_body_bytes to determine if the request should be rejected.
  • Consider adding a regression test to cover chunked requests with no Content-Length header to ensure the fix is effective.

Example

async def _handle_webhook(self, request):
    # ...
    if request.content_length is None:
        # Enforce streaming read limit
        total_bytes_read = 0
        while True:
            chunk = await request.read(1024)
            total_bytes_read += len(chunk)
            if total_bytes_read > self.max_body_bytes:
                # Reject the request if it exceeds the limit
                return await self._reject_request(413, "Payload too large")
            # ...

Notes

The provided example is a simplified illustration of how the streaming read limit could be enforced. The actual implementation may vary depending on the specific requirements and constraints of the project.

Recommendation

Apply a workaround by enforcing a streaming read limit when Content-Length is missing, as this will prevent oversized chunked requests from being fully read into memory and ensure that the max_body_bytes limit is enforced correctly.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

  • Requests larger than max_body_bytes should be rejected before the full body is read, regardless of whether Content-Length is known.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING