The receiver's 1 MB body cap lives in the handler, not the middleware the other route groups use #173

Closed
opened 2026-08-18 00:41:49 +02:00 by clawbot · 2 comments
Collaborator

Found by the independent review of #167 while verifying the premise that route's memory bound rests on. Not a defect — the cap is real and effective — so deliberately NOT milestoned 1.0.0.

/webhook/{uuid} carries only ReceiverRateLimit(). Unlike the four page route groups, it has no MaxBodySize middleware. Its 1 MB cap is enforced inside the handler instead, at internal/handlers/webhook.go:145-166, via io.LimitReader plus a length check.

That works today and the reviewer confirmed it. The problem is the asymmetry is undocumented and invisible at the routing layer: someone reading internal/server/routes.go sees four groups with an explicit body-size middleware and one without, and would reasonably conclude the receiver is uncapped — or, worse, edit the handler assuming middleware coverage that is not there and silently remove the only bound on the one unauthenticated, internet-facing endpoint.

Several units this cycle have depended on that 1 MB cap being real: #135 sized the event-log render bound from it, and #157's whole memory argument rests on it.

Definition of done

Either:

  • Move the receiver onto the same MaxBodySize middleware the page groups use, so the cap is visible where every other route's cap is — checking first that this does not change the 413 semantics or the error the handler currently returns, since the receiver's response shape is part of its contract with senders; or
  • Keep it in the handler and add a comment at the route registration in internal/server/routes.go saying where the cap actually lives and why, so the omission reads as deliberate rather than forgotten.

State which you chose and why. If you move it, add a test asserting an oversized body is rejected at the middleware rather than the handler; if you document it, add a test that pins the handler-level cap so it cannot be removed silently.

Implementation requirements

  • Branch from next, PR based on next, single commit, title ending (closes #N).
  • Do not modify TODO.md (see #112).
  • Run make bootstrap in a fresh clone before gating — browser assets are fetched at build time and not committed.
  • Gate on make check plus the Docker lint path with the cache defeated. All linting runs in Docker, never on the host.
Found by the independent review of https://git.eeqj.de/sneak/webhooker/pulls/167 while verifying the premise that route's memory bound rests on. Not a defect — the cap is real and effective — so deliberately NOT milestoned 1.0.0. `/webhook/{uuid}` carries only `ReceiverRateLimit()`. Unlike the four page route groups, it has **no `MaxBodySize` middleware**. Its 1 MB cap is enforced inside the handler instead, at `internal/handlers/webhook.go:145-166`, via `io.LimitReader` plus a length check. That works today and the reviewer confirmed it. The problem is the asymmetry is undocumented and invisible at the routing layer: someone reading `internal/server/routes.go` sees four groups with an explicit body-size middleware and one without, and would reasonably conclude the receiver is uncapped — or, worse, edit the handler assuming middleware coverage that is not there and silently remove the only bound on the one unauthenticated, internet-facing endpoint. Several units this cycle have depended on that 1 MB cap being real: https://git.eeqj.de/sneak/webhooker/issues/135 sized the event-log render bound from it, and https://git.eeqj.de/sneak/webhooker/issues/157's whole memory argument rests on it. ## Definition of done Either: - Move the receiver onto the same `MaxBodySize` middleware the page groups use, so the cap is visible where every other route's cap is — checking first that this does not change the 413 semantics or the error the handler currently returns, since the receiver's response shape is part of its contract with senders; or - Keep it in the handler and add a comment at the route registration in `internal/server/routes.go` saying where the cap actually lives and why, so the omission reads as deliberate rather than forgotten. State which you chose and why. If you move it, add a test asserting an oversized body is rejected at the middleware rather than the handler; if you document it, add a test that pins the handler-level cap so it cannot be removed silently. ## Implementation requirements - Branch from `next`, PR based on `next`, single commit, title ending ` (closes #N)`. - Do not modify `TODO.md` (see https://git.eeqj.de/sneak/webhooker/issues/112). - Run `make bootstrap` in a fresh clone before gating — browser assets are fetched at build time and not committed. - Gate on `make check` plus the Docker lint path with the cache defeated. All linting runs in Docker, never on the host.
clawbot self-assigned this 2026-08-18 00:41:49 +02:00
Author
Collaborator

Plan. The receiver is now /h/{uuid} (#367). Take the issue's second option: keep the 1 MB cap in the handler, which owns the response senders see, and add a comment at the route registration in internal/server/routes.go saying where the cap lives and why the receiver has no MaxBodySize. A test sends a body one byte over the cap through the production router and pins the refusal (status and body), so the cap cannot be removed silently; if such a test already exists, the comment names it. The first option would route a refusal through middleware that, since #382, renders error pages for admin routes, and would risk changing the response senders get.

Model: opus-5-5

Plan. The receiver is now `/h/{uuid}` (https://git.eeqj.de/sneak/webhooker/issues/367). Take the issue's second option: keep the 1 MB cap in the handler, which owns the response senders see, and add a comment at the route registration in `internal/server/routes.go` saying where the cap lives and why the receiver has no `MaxBodySize`. A test sends a body one byte over the cap through the production router and pins the refusal (status and body), so the cap cannot be removed silently; if such a test already exists, the comment names it. The first option would route a refusal through middleware that, since https://git.eeqj.de/sneak/webhooker/issues/382, renders error pages for admin routes, and would risk changing the response senders get. Model: opus-5-5
Author
Collaborator

#429 keeps the receiver's 1 MB body cap in the handler, as planned. The /h/{uuid} registration in internal/server/routes.go now has a comment saying where the cap lives and why the route has no MaxBodySize. The handler's body-reading function says it is the receiver's only cap. No existing test pinned the cap, so a new routing test, TestReceiver_OversizeBodyRefused, sends a body of exactly 1 MB and one a byte over through the production router and checks that the second gets the handler's 413 and message.

Judgement call: the test writes the cap as a literal 1 MB rather than reading the handler's unexported constant, so changing the cap means changing the test too.

Model: opus-5-5

https://git.eeqj.de/sneak/webhooker/pulls/429 keeps the receiver's 1 MB body cap in the handler, as planned. The `/h/{uuid}` registration in `internal/server/routes.go` now has a comment saying where the cap lives and why the route has no `MaxBodySize`. The handler's body-reading function says it is the receiver's only cap. No existing test pinned the cap, so a new routing test, `TestReceiver_OversizeBodyRefused`, sends a body of exactly 1 MB and one a byte over through the production router and checks that the second gets the handler's 413 and message. Judgement call: the test writes the cap as a literal 1 MB rather than reading the handler's unexported constant, so changing the cap means changing the test too. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/webhooker#173