Say at the receiver route where its 1 MB body cap lives (closes #173)
check / check (push) Successful in 3m22s

The receiver route /h/{uuid} has no MaxBodySize middleware, unlike the page route groups; its 1 MB cap is enforced in the handler, which owns the response senders see. Nothing at the route said so, so a reader could take the receiver for uncapped or remove the only bound on the one unauthenticated endpoint. The route registration and the handler's body-reading function now say where the cap lives and why. A routing test sends a body of exactly 1 MB and one a byte over through the production router and pins that only the second is refused, with the handler's 413 and message.

Model: opus-5-5
This commit was merged in pull request #429.
This commit is contained in:
2026-10-02 13:40:00 +02:00
parent fd036774f9
commit 5551f75251
3 changed files with 56 additions and 1 deletions
+3 -1
View File
@@ -150,7 +150,9 @@ func (h *Handlers) lookupEntrypoint(
return entrypoint, true return entrypoint, true
} }
// readWebhookBody reads and validates the request body size. // readWebhookBody reads and validates the request body size. This is
// the receiver's only body cap: /h/{uuid} has no MaxBodySize
// middleware (see Server.setupWebhookRoutes).
func (h *Handlers) readWebhookBody( func (h *Handlers) readWebhookBody(
w http.ResponseWriter, w http.ResponseWriter,
r *http.Request, r *http.Request,
+6
View File
@@ -324,6 +324,12 @@ func (s *Server) setupSourceRoutes() {
} }
func (s *Server) setupWebhookRoutes() { func (s *Server) setupWebhookRoutes() {
// No MaxBodySize here, unlike the page groups. The receiver's 1 MB
// body cap is in Handlers.readWebhookBody, because the handler
// owns the response a sender gets for an oversized body and
// MaxBodySize would change it. That cap is the only bound on this
// unauthenticated endpoint's body; TestReceiver_OversizeBodyRefused
// pins it.
s.router.With(s.mw.ReceiverRateLimit()).HandleFunc( s.router.With(s.mw.ReceiverRateLimit()).HandleFunc(
"/h/{uuid}", "/h/{uuid}",
s.h.HandleWebhook(), s.h.HandleWebhook(),
+47
View File
@@ -1428,6 +1428,53 @@ func TestReceiver_EntrypointURLIsRateLimited(t *testing.T) {
) )
} }
// TestReceiver_OversizeBodyRefused pins the receiver's 1 MB body
// cap, which lives in the handler rather than in a MaxBodySize
// middleware. A body exactly at the cap is accepted; one byte over is
// refused with the handler's own 413.
func TestReceiver_OversizeBodyRefused(t *testing.T) {
t.Parallel()
const bodyCap = 1 << 20 // 1 MB
env := newTestEnvWithConfig(t, &config.Config{
DataDir: t.TempDir(),
Environment: config.EnvironmentDev,
ReceiverRateLimit: 10,
})
userID, _ := env.seedUser(t, "receiver", "somepassword")
wh := env.seedWebhook(t, userID)
require.NoError(t, env.db.DB().Omit(clause.Associations).Create(
&database.Entrypoint{
WebhookID: wh.ID,
Path: "0b7c3e5a-2d9f-4a61-8e4b-7c1d6f2a9e35",
Active: true,
},
).Error)
send := func(size int) *httptest.ResponseRecorder {
req := httptest.NewRequestWithContext(
context.Background(), http.MethodPost,
"/h/0b7c3e5a-2d9f-4a61-8e4b-7c1d6f2a9e35",
strings.NewReader(strings.Repeat("a", size)),
)
w := httptest.NewRecorder()
env.router.ServeHTTP(w, req)
return w
}
assert.Equal(
t, http.StatusOK, send(bodyCap).Code,
"a body exactly at the cap must be accepted",
)
w := send(bodyCap + 1)
assert.Equal(t, http.StatusRequestEntityTooLarge, w.Code)
assert.Equal(t, "Request body too large\n", w.Body.String())
}
// metricsConfig is a Config differing from the routing default only // metricsConfig is a Config differing from the routing default only
// in the two /metrics credentials. // in the two /metrics credentials.
func metricsConfig( func metricsConfig(