Repository navigation
Description
Problem
The mcpx daemon enforces its configured HTTP request-body ceilings (http.bodyLimit / http.callBodyLimit / http.controlBodyLimit, defaults in pkgs/mcpx/internal/defaults/defaults.go:162,186-195) on only some routes. A second set of handlers decodes or reads r.Body with no ceiling at all, and there is no global cap: the two http.Servers (pkgs/mcpx/internal/daemon/server.go:307,360) set only ReadHeaderTimeout, with no MaxBytesHandler. The defaults module's own comment states the intent — the ceiling was unified precisely because it used to differ per route — but the unification never reached these handlers.
Verified on main 264ba11549ddff607a38c72ff043e0cd0e7ef247:
Unbounded body reads (no MaxBytesReader, no limit):
routes_proxy.go:44—handleChatCompletions:io.ReadAll(r.Body)buffers the whole chat-completions request in memoryupgrade.go:106—handleUpgrade:io.ReadAll(r.Body)routes_consumer.go:45—handleDiagnose:json.NewDecoder(r.Body).Decoderoutes_consumer.go:203—handleRecipeSave: sameroutes_consumer.go:278—handleRecipeRun: sameroutes_consumer.go:369—handleIntent: sameroutes_embeddings.go:40—handleEmbeddings: sameroutes_exec.go:218—handleExec: sameroutes_feedback.go:32—handleFeedbackSubmit: sameserver.go:616—POST /v1/prompthandler: same
Bounded siblings (the house pattern these miss):
server.go:870(http.callBodyLimit),server.go:963,server.go:1083(http.bodyLimit),server.go:1100(http.controlBodyLimit)routes_secrets.go:49,114(http.bodyLimit),routes_settings.go:200(http.controlBodyLimit),routes_settings.go:588,ops.go:336routes_proto.go:400(defaults.HTTPCallBodyLimit),routes_proto.go:627,routes_proto.go:879
Why it matters
The daemon listens on loopback TCP + a unix socket. Any local process — or any local client with a bug, e.g. a runaway script POSTing an ever-growing prompt/exec/chat body — can make the daemon buffer an unbounded request body and OOM the process that every other tool call depends on. handleChatCompletions is the worst case: it io.ReadAlls the entire body before parsing. The configured ceilings exist and are already honoured on the call/proto/secrets routes; the gap is pure inconsistency, and it is exactly the class #1893 asks to be guarded (a declared surface — "every body is capped" — with no guard walking it). Related but distinct: #1697 (settings byte-size values themselves accept negatives/overflow).
Acceptance criteria
- Every daemon HTTP handler that reads a request body enforces a configured ceiling (
http.MaxBytesReaderor equivalent) with the limit for its route class (call / control / body), closing the enumerated list above; triage pins which class each route takes where ambiguous (chat completions and exec are call-shaped; upgrade/diagnose/recipes/intent/embeddings/feedback/prompt need an explicit ruling). - An over-limit body gets a clean client error (400/413) and the daemon's memory stays bounded — no partial decode, no panic.
- Tests: an over-limit request to representative previously-unbounded routes (at least chat completions, exec, intent, upgrade) is refused; under-limit requests to those routes behave exactly as before.
- A guard (test or lint-style check, in the spirit of mcpx: a promise and a behaviour that disagree — every declared surface needs a guard that walks it (tracker) #1893 / mcpx testing: guard tests that close a whole bug class #1971) fails when a daemon handler reads
r.Bodywithout a ceiling, so the next route cannot reintroduce the gap. - Existing daemon test suites pass unchanged.
Activity
developing-today commentedon Oct 29, 2026
CODE-997
dezren39 commentedon Oct 29, 2026
🤖 Agent note · filed
264ba115that a second set of mcpx daemon handlers reads request bodies with no size ceiling while sibling routes enforce the configuredhttp.*BodyLimitcaps viahttp.MaxBytesReader: unboundedio.ReadAllat routes_proxy.go:44 (chat completions) and upgrade.go:106; unboundedjson.NewDecoder(r.Body)at routes_consumer.go:45/203/278/369, routes_embeddings.go:40, routes_exec.go:218, routes_feedback.go:32, server.go:616 (POST /v1/prompt). No global cap exists (server.go:307,360 set only ReadHeaderTimeout). Dedup: title+keyword search over open and closed issues — nearest mcpx(settings): byte sizes accept negatives and int64 overflow — http.streamBufferInit=-1 passes validation, then panics the CLI with 'makeslice: cap out of range' at the first streaming read #1697 (settings byte-size validation, distinct) and mcpx: a promise and a behaviour that disagree — every declared surface needs a guard that walks it (tracker) #1893 (guard tracker, this is a concrete instance).hidden_files/queue-pilot/packet-2082-triage.json(repo-health-code-pilot, triage); bodyhidden_files/issue-code-mcpx-daemon-body-limits-body.md3 remaining items
dezren39 commentedon Nov 1, 2026
🤖 Agent note · picked up
Merge pull request #3370 from developing-today/queue/5079-2082-body-l…
dezren39 commentedon Nov 1, 2026
🤖 Agent note · done — fixed and merged
ee8bc80c18fe30c7e1f70989d9e8342f1c110a59(branchqueue/5079-2082-body-limits, commitbf851eb81)http.MaxBytesReaderat the triage-pinned classes (call: chat completions, embeddings, exec, intent, recipe run; body: diagnose, recipe save, feedback, POST /v1/prompt; control: upgrade), plus one extra site found in implementation —handleEmbeddingsImporthandingr.Bodyto the whole-bodyVectorIndex.Importdecode (call class). Over-limit surfaces as 400 through each handler's existing error path, matching the bounded routes; no status codes changed. Limits resolve via a new nil-safeServer.bodyByteshelper (thehandleBatchfallback shape) — without it the wraps nil-panicked in existing bare-&Server{}tests; the four pre-existing server.go wraps moved onto the same helper.go vet ./internal/daemon/clean; newbody_limits_test.go(over-limit 400s for chat/exec/intent/upgrade, under-limit unchanged,TestNoUnboundedBodyReadersguard) passes; fullinternal/daemonsuite passes (ok 129.719s). Codex review on fix(mcpx/daemon): MaxBytesReader ceilings for the unbounded body readers (#2082) #3370 completed with no findings.