Skip to content

fix(core): guide plugin route handlers to ctx.input on consumed body (#1293)#1555

Merged
ascorbic merged 1 commit into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1293-plugin-request-body-guard
Jun 24, 2026
Merged

fix(core): guide plugin route handlers to ctx.input on consumed body (#1293)#1555
ascorbic merged 1 commit into
emdash-cms:mainfrom
Emdash-Bug-Testing:fix/1293-plugin-request-body-guard

Conversation

@marcusbellamyshaw-cell

@marcusbellamyshaw-cell marcusbellamyshaw-cell commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

EmDash parses a plugin route's request body once before the handler runs and exposes it as ctx.input, leaving ctx.request's stream consumed. A handler that instinctively called ctx.request.json() (or .text() / .formData() / .arrayBuffer() / .blob()) re-read a spent stream and got the runtime's opaque Body has already been read error with no mention of ctx.input.

This wraps the request handed to handlers in a Proxy that intercepts the body-reading methods and throws an actionable [emdash] message naming ctx.input. Every non-body member (url, method, headers, signal, …) passes through unchanged, and function members are bound to the underlying request so methods like clone() and the sandbox-entry adapter's headers.forEach keep working. Also updates the two plugin docs that demonstrated the ctx.request.json() footgun.

Closes #1293

Type of change

  • Bug fix

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (targeted: plugins/routes.test.ts + full plugins unit/integration suites — 702 tests)
  • pnpm format has been run
  • I have added/updated tests for my changes
  • User-visible strings in the admin UI are wrapped for translation (n/a — server-side English diagnostic message)
  • I have added a changeset

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 ultracode

Screenshots / test output

New tests assert every guarded body method rejects with a message containing ctx.input and [emdash], that ctx.input still holds the parsed body, and that url / method / headers.get / headers.forEach still pass through the guard. Plugins suites: 702 passed.

…mdash-cms#1293)

EmDash parses a plugin route's request body once and exposes it as
ctx.input, leaving ctx.request's stream consumed. A handler that
instinctively called ctx.request.json() (or .text()/.formData()/etc.)
got the runtime's opaque "Body has already been read" error with no hint
about ctx.input.

Wrap the request handed to handlers in a Proxy that intercepts the
body-reading methods and throws an actionable [emdash] message naming
ctx.input. Non-body members pass through unchanged; function members are
bound to the underlying request so methods like clone() and the
sandbox-entry adapter's headers.forEach keep working. Also update the
plugin docs that demonstrated the ctx.request.json() footgun.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jun 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fbe8ed4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jun 24, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right fix for the right problem. EmDash pre-parses the request body for plugin routes and hands the same Request object to handlers, so calling ctx.request.json() re-reads a consumed stream and produces an opaque platform error. Wrapping the handler-facing request in a Proxy that intercepts only the body-reading methods, passes everything else through, and throws an actionable [emdash] message pointing to ctx.input is a clean, additive approach that matches the architecture.

I read the diff, the full packages/core/src/plugins/routes.ts, the new unit tests, and traced the callers (PluginManager.invokeRoute, handlePluginApiRoute, adapt-sandbox-entry.ts). The guarded request is correctly applied only in the trusted-plugin route path; the sandboxed route path already forwards a serialized { url, method, headers, meta } shape, so it is unaffected. The docs are updated to steer plugin authors toward ctx.input, and a changeset is present.

One tiny non-blocking note: the RouteContext.request docstring in packages/core/src/plugins/types.ts still says “Original request,” but it is now a guarded Proxy of the original. Worth updating the comment to match the new semantics.

I did not run the test suite/linter/build; this review is static. Based on the code, the implementation is sound and I would sign off.

@pkg-pr-new

pkg-pr-new Bot commented Jun 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1555

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1555

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1555

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1555

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1555

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1555

emdash

npm i https://pkg.pr.new/emdash@1555

create-emdash

npm i https://pkg.pr.new/create-emdash@1555

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1555

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1555

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1555

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1555

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1555

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1555

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1555

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1555

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1555

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1555

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1555

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1555

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1555

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1555

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1555

commit: fbe8ed4

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-review No maintainer or bot review yet labels Jun 24, 2026
@ascorbic ascorbic merged commit 55613e1 into emdash-cms:main Jun 24, 2026
45 checks passed
@emdashbot emdashbot Bot mentioned this pull request Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core bot:review Trigger an emdashbot code review on this PR cla: signed review/approved Approved; no new commits since size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(plugins): ctx.request.json() in route handlers gives cryptic 'body already consumed' error instead of pointing to ctx.input

2 participants