Skip to content

feat(rsc): allow rejecting server references during decodeReply/decodeAction - #1520

Merged
hi-ogawa merged 16 commits into
vitejs:mainfrom
james-elicx:feat/rsc-on-server-reference
Oct 20, 2026
Merged

hi-ogawa merged 16 commits into
vitejs:mainfrom
james-elicx:feat/rsc-on-server-reference

Conversation

@james-elicx

@james-elicx james-elicx commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Description

decodeReply, decodeAction, and decodeFormState always decode with the plugin's server manifest, which resolves any server reference the client sends. A framework can't limit which server functions a request is allowed to decode, for example to the actions its current route exposes. Next.js does this with a page-scoped server module map, so a server function passed as an argument, or bound to a form action, has to belong to the current page.

Today a framework has to skip these wrappers and call @vitejs/plugin-rsc/vendor/react-server-dom/server.edge directly with its own manifest built on top of createServerManifest().

This adds an experimental onServerReference callback in an optional last parameter, alongside the existing onClientReference extension on renderToReadableStream:

decodeReply(body, { temporaryReferences }, { onServerReference })
decodeAction(formData, { onServerReference })
decodeFormState(result, formData, { onServerReference })

The callback receives { id, name } for each server reference React resolves while decoding. That includes the action itself in decodeAction, and any server function passed as an argument or bound to an action. Throwing rejects the reference before its module loads, and decoding fails. decodeReplyFromBusboy in @vitejs/plugin-rsc/rsc/server.node gets the same parameter. Without the callback, nothing changes.

loadServerAction doesn't call it, so for fetch actions a framework still checks the action id itself. The README says so.

Tests

New e2e/server-reference-callback.test.ts, an inline starter-extra fixture, in dev and build. Its RSC entry rejects one named server reference. Two forms bind a recording action to an allowed and a rejected server function, and each is submitted twice:

  • with JS (decodeReply);
  • without JS (decodeAction).

The rejected submissions fail with a 500 and never run the action. The allowed ones run with the bound server function. Without the source change, the rejected submissions return 200 and run the action.

decodeFormState passes the callback through too, but the test doesn't exercise it. React only decodes there for useActionState forms ($ACTION_KEY), and decodeAction already rejects the same bound references first.

@james-elicx james-elicx changed the title feat(rsc): add onServerReference callback to decode APIs feat(rsc): allow rejecting server references during decodeReply/decodeAction Oct 19, 2026
@james-elicx
james-elicx marked this pull request as ready for review October 19, 2026 10:56
hi-ogawa and others added 14 commits October 20, 2026 11:39
…tion-reachability example

The progressive form path rejects server references that the current route cannot reach through the `onServerReference` callback of `decodeAction` and `decodeFormState`, so React's own decoder does the checking instead of a pre-decode copy of its `$ACTION_*` field parsing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…example

`decodeReply` also receives the `onServerReference` validator, so a server function passed as an action argument has to be reachable from the route that runs the action, just like the progressive path. New e2e cases cover a retained action passed as an argument from another route, and a progressive form whose shared action is reachable from the replay target while the server function bound to it is not.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The README walks through manifest generation, request redispatch, and the decode-time check for nested server references and progressive forms, each with a figure. It also links the comparable Next.js behavior for progressive form validation and for nested server references after navigation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ppers

`src/types` mirrors upstream React types, so the Vite-specific `extraOptions` parameter is declared on the `decodeReply` and `decodeReplyFromBusboy` wrappers, like `renderToReadableStream`'s, instead of on `DecodeReplyFunction` and `DecodeReplyFromBusboyFunction`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…action-reachability example

`decodeFormState` decodes the last `$ACTION_REF_*` field, while `decodeAction` may use a later `$ACTION_ID_*` field, so a crafted progressive body can reach a server reference that `decodeAction` never checked. The example responds with 404 when that check fails, and a new e2e case posts such a body built from the server-rendered `/b` and `/c` forms.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ty example

`onServerReference` runs for every server reference React resolves through the manifest, so `decodeFormState` is held to the same route by construction.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@hi-ogawa hi-ogawa 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.

Thanks! This API makes sense. I updated examples/action-reachability to use this API instead of probing FormData and this also now covers the case of hydrated action arguments being unreachable actions.

@hi-ogawa
hi-ogawa merged commit 505da21 into vitejs:main Oct 20, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants