Skip to content

Commit 41a0063

Browse files
committed
admin: oversized create-table body returns 413, not 400 (Codex P2)
decodeCreateTableRequest used to surface every read/parse failure as the same generic "invalid_body" string, so handleCreate mapped all of them to 400 — including the BodyLimit/MaxBytesReader overflow that should produce 413 payload_too_large. The middleware contract in internal/admin/middleware.go promises 413 on oversized bodies (WriteMaxBytesError lives in that file exactly for this purpose). Codex P2 on PR #634 flagged the write path as the only handler that broke that contract: callers and retry logic could not distinguish "body too big" from "body malformed", and oversize requests would be retried as if a caller-side fix was possible. Fix: introduce errCreateBodyTooLarge as a sentinel returned only when io.ReadAll trips MaxBytesReader. handleCreate matches the sentinel via errors.Is and routes to WriteMaxBytesError, which emits the canonical 413 + payload_too_large body. All other decode paths still produce 400 invalid_body unchanged. Test: TestDynamoHandler_CreateTable_OversizedBodyReturns413 wraps the request body in MaxBytesReader (mirroring what the real BodyLimit middleware does) and confirms the response is 413 with a payload_too_large code. Also asserts the stub source is not touched on rejection.
1 parent dcac6e4 commit 41a0063

2 files changed

Lines changed: 44 additions & 1 deletion

File tree

internal/admin/dynamo_handler.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,15 @@ var (
133133
ErrTablesAlreadyExists = errors.New("admin tables: table already exists")
134134
)
135135

136+
// errCreateBodyTooLarge is returned by decodeCreateTableRequest
137+
// when the request body trips the BodyLimit middleware's
138+
// MaxBytesReader. The handler matches this sentinel to map the
139+
// failure to 413 payload_too_large rather than the generic 400
140+
// invalid_body — the BodyLimit/middleware contract documented in
141+
// internal/admin/middleware.go (Codex P2 on PR #634 flagged the
142+
// previous always-400 behaviour as a regression).
143+
var errCreateBodyTooLarge = errors.New("request body exceeds the 64 KiB admin limit")
144+
136145
// ValidationError is what the source returns when the input fails
137146
// adapter-side validation. Surfaces a sanitised message back to the
138147
// SPA — adapter-internal err.Error() output is never sent verbatim.
@@ -280,6 +289,10 @@ func (h *DynamoHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
280289
}
281290
body, err := decodeCreateTableRequest(r.Body)
282291
if err != nil {
292+
if errors.Is(err, errCreateBodyTooLarge) {
293+
WriteMaxBytesError(w)
294+
return
295+
}
283296
writeJSONError(w, http.StatusBadRequest, "invalid_body", err.Error())
284297
return
285298
}
@@ -362,7 +375,9 @@ func decodeCreateTableRequest(body io.Reader) (CreateTableRequest, error) {
362375
raw, err := io.ReadAll(body)
363376
if err != nil {
364377
if IsMaxBytesError(err) {
365-
return CreateTableRequest{}, errors.New("request body exceeds the 64 KiB admin limit")
378+
// Sentinel so handleCreate can map to 413 rather than
379+
// the generic 400 invalid_body.
380+
return CreateTableRequest{}, errCreateBodyTooLarge
366381
}
367382
return CreateTableRequest{}, errors.New("request body could not be read")
368383
}

internal/admin/dynamo_handler_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,34 @@ func TestDynamoHandler_CreateTable_RejectsMissingPrincipal(t *testing.T) {
487487
require.Equal(t, http.StatusUnauthorized, rec.Code)
488488
}
489489

490+
// TestDynamoHandler_CreateTable_OversizedBodyReturns413 confirms a
491+
// body that trips BodyLimit's MaxBytesReader surfaces as 413
492+
// payload_too_large rather than the generic 400 invalid_body
493+
// (Codex P2 on PR #634). The middleware contract in
494+
// internal/admin/middleware.go is that oversized bodies map to
495+
// 413; the previous wholesale "decode failure → 400" path
496+
// silently broke that for this endpoint.
497+
func TestDynamoHandler_CreateTable_OversizedBodyReturns413(t *testing.T) {
498+
src := &stubTablesSource{tables: map[string]*DynamoTableSummary{}}
499+
h := newDynamoHandlerForTest(src)
500+
// Build a body just over the limit. Padding a real-shape
501+
// JSON object with whitespace keeps the structure valid up
502+
// to the cap so the test isolates the size-rejection path.
503+
oversize := `{"table_name":"u","partition_key":{"name":"id","type":"S"}` +
504+
strings.Repeat(" ", int(defaultBodyLimit)+1) + "}"
505+
req := httptest.NewRequest(http.MethodPost, pathDynamoTables, strings.NewReader(oversize))
506+
req = withWritePrincipal(req)
507+
// The router applies BodyLimit at the outer wrap; emulate
508+
// that here so MaxBytesReader is in play during ReadAll.
509+
req.Body = http.MaxBytesReader(httptest.NewRecorder(), req.Body, defaultBodyLimit)
510+
rec := httptest.NewRecorder()
511+
h.ServeHTTP(rec, req)
512+
513+
require.Equal(t, http.StatusRequestEntityTooLarge, rec.Code)
514+
require.Contains(t, rec.Body.String(), "payload_too_large")
515+
require.Empty(t, src.lastCreateInput.TableName, "source must not be touched on oversized body")
516+
}
517+
490518
func TestDynamoHandler_CreateTable_RejectsBadJSON(t *testing.T) {
491519
src := &stubTablesSource{tables: map[string]*DynamoTableSummary{}}
492520
h := newDynamoHandlerForTest(src)

0 commit comments

Comments
 (0)