Skip to content

mcpx(daemon): a second set of routes reads request bodies with no size ceiling — chat completions, exec, intent, upgrade and five more bypass the configured http.*BodyLimit caps #2082

Closed

Description

@dezren39

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 memory
  • upgrade.go:106 — handleUpgrade: io.ReadAll(r.Body)
  • routes_consumer.go:45 — handleDiagnose: json.NewDecoder(r.Body).Decode
  • routes_consumer.go:203 — handleRecipeSave: same
  • routes_consumer.go:278 — handleRecipeRun: same
  • routes_consumer.go:369 — handleIntent: same
  • routes_embeddings.go:40 — handleEmbeddings: same
  • routes_exec.go:218 — handleExec: same
  • routes_feedback.go:32 — handleFeedbackSubmit: same
  • server.go:616 — POST /v1/prompt handler: 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:336
  • routes_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.MaxBytesReader or 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.Body without a ceiling, so the next route cannot reintroduce the gap.
  • Existing daemon test suites pass unchanged.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    on Oct 29, 2026
  2. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  3. added
    queue/queuedHas a queued task in the Supabase agent queue
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 29, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  5. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  6. 3 remaining items

  7. added
    queue/queuedHas a queued task in the Supabase agent queue
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. added a commit that references this issue on Nov 1, 2026
  10. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done — fixed and merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions