Summary
Calling a 'use server' function directly from an App Route handler (app/api/**/route.ts) and rendering react-dom/server.edge from inside the same route handler are mutually exclusive in vinext today. Whichever Vite environment the route handler loads in breaks one or the other.
This is a follow-up to #882 / #1237 (react-dom/server in route handlers) — fixing those bugs by loading route handlers in the SSR env introduces a new bug: server functions throw noServerCall.
Repro
A typical sign-in flow that does both — call a server action to provision the account, then send a welcome email:
// app/api/auth/session/route.ts
import { createAccount } from '@/lib/actions/accounts/createAccount';
import { sendPaidWelcomeEmail } from '@/lib/email';
export async function POST(req: NextRequest) {
const decoded = await firebaseAdmin.verifyIdToken(...);
await createAccount(decoded); // <- 'use server' function
await sendPaidWelcomeEmail({...}); // <- uses @react-email/render -> react-dom/server.edge
...
}
// lib/actions/accounts/createAccount.ts
'use server';
export async function createAccount(decoded: DecodedIdToken) { ... }
// lib/email.ts
import { render } from '@react-email/render';
// render() does: import("react-dom/server.edge").catch(() => import("react-dom/server"))
Reproduces on vinext@0.0.50, @vitejs/plugin-rsc@0.5.23, @react-email/render@1.4.0, react@19.2.6, with the Cloudflare plugin (viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] }).
What goes wrong, and where
Path A — route handler loads in the RSC env (current vinext default)
react-dom/server.edge resolution under react-server condition picks server.react-server.js, which throws:
Error: react-dom/server is not supported in React Server Components.
at runInRunnerObject (workers/runner-worker/index.js:107:3)
at render (@react-email/render/src/edge/render.tsx:13:26)
...
at Object.POST [as handlerFn] (app/api/auth/session/route.ts:30:11)
at runAppRouteHandler (vinext/src/server/app-route-handler-execution.ts:127:20)
Even if you alias react-dom/server.edge → absolute path (the workaround in #1237), the inner require('react') inside react-dom-server.edge.development.js:8817 is still resolved by the RSC env's resolver with the react-server condition. It picks the react-server variant of react, which exports __SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE instead of __CLIENT_INTERNALS_…. Then at line 9533 ReactSharedInternals is undefined and the next write throws:
TypeError: Cannot set properties of undefined (setting 'recentlyCreatedOwnerStacks')
at resetOwnerStackLimit (react-dom/cjs/react-dom-server.edge.development.js:4534)
So the #1237 alias workaround is incomplete. I verified this on a fresh repro.
Path B — route handler loads in the SSR env (workaround patch)
Patching vinext so dispatchMatchedRouteHandler loads the route module via import.meta.viteRsc.loadModule(\"ssr\", \"index\") (and the SSR entry statically imports all route modules) fixes the react-dom/server.edge problem cleanly — verified, @react-email/render renders successfully and the email lands in the inbox via the live Cloudflare send_email binding.
But it introduces a new bug: plugin-rsc's SSR-env 'use server' transform replaces the function body with a server-reference stub backed by callServer = null. The route handler imports createAccount and gets the stub. Calling it throws:
Error: Server Functions cannot be called during initial render. ...
at noServerCall (@vitejs/plugin-rsc/vendor/react-server-dom/client.edge.js:2405)
at action (..../client.edge.js:601)
at Object.POST [as handlerFn] (app/api/auth/session/route.ts:30:11)
transformProxyExport in @vitejs/plugin-rsc/dist/transforms/index.js:284-356 removes non-export declarations, so the real function isn't reachable from the SSR-transformed module.
Why neither env works
| Env where route handler runs |
react-dom/server.edge |
'use server' direct call |
| RSC (default) |
❌ react-server condition picks throwing file; aliasing only the entrypoint isn't enough because nested require('react') still resolves to the react-server variant |
✅ registerServerReference keeps the real function callable |
| SSR (patched) |
✅ no react-server condition |
❌ createServerReference returns a stub; callServer is null |
Route handlers are server-only — the client→server bridge is irrelevant for them, but vinext is forced to pick one of two envs that each break a real-world case.
Suggested fixes (in increasing order of invasiveness)
- Document
'use server' is unsupported in route-handler-imported files when using vinext, and recommend dropping the directive for functions only called server-side. (Mechanical fix; works today.)
- In the SSR env, expose the real function alongside the server reference for
'use server' files. This needs a plugin-rsc change, not a vinext change.
- Add a dedicated "route handler" Vite env that has neither
react-server condition nor plugin-rsc's SSR 'use server' transform. App Route handlers load from there. Both cases work.
(3) feels like the architecturally correct answer — route handlers are a distinct execution context that doesn't fit cleanly in either RSC (render) or SSR (decode-and-render) semantics.
Environment
- vinext 0.0.50
- @vitejs/plugin-rsc 0.5.23
- @cloudflare/vite-plugin (latest)
- Vite 8.0.13
- react 19.2.6 / react-dom 19.2.6
- @react-email/render 1.4.0
- Cloudflare Workers via
viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] }
Summary
Calling a
'use server'function directly from an App Route handler (app/api/**/route.ts) and renderingreact-dom/server.edgefrom inside the same route handler are mutually exclusive in vinext today. Whichever Vite environment the route handler loads in breaks one or the other.This is a follow-up to #882 / #1237 (react-dom/server in route handlers) — fixing those bugs by loading route handlers in the SSR env introduces a new bug: server functions throw
noServerCall.Repro
A typical sign-in flow that does both — call a server action to provision the account, then send a welcome email:
Reproduces on
vinext@0.0.50,@vitejs/plugin-rsc@0.5.23,@react-email/render@1.4.0,react@19.2.6, with the Cloudflare plugin (viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] }).What goes wrong, and where
Path A — route handler loads in the RSC env (current vinext default)
react-dom/server.edgeresolution underreact-servercondition picksserver.react-server.js, which throws:Even if you alias
react-dom/server.edge→ absolute path (the workaround in #1237), the innerrequire('react')insidereact-dom-server.edge.development.js:8817is still resolved by the RSC env's resolver with thereact-servercondition. It picks thereact-servervariant ofreact, which exports__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADEinstead of__CLIENT_INTERNALS_…. Then at line 9533ReactSharedInternalsis undefined and the next write throws:So the #1237 alias workaround is incomplete. I verified this on a fresh repro.
Path B — route handler loads in the SSR env (workaround patch)
Patching vinext so
dispatchMatchedRouteHandlerloads the route module viaimport.meta.viteRsc.loadModule(\"ssr\", \"index\")(and the SSR entry statically imports all route modules) fixes thereact-dom/server.edgeproblem cleanly — verified,@react-email/renderrenders successfully and the email lands in the inbox via the live Cloudflaresend_emailbinding.But it introduces a new bug: plugin-rsc's SSR-env
'use server'transform replaces the function body with a server-reference stub backed bycallServer = null. The route handler importscreateAccountand gets the stub. Calling it throws:transformProxyExportin@vitejs/plugin-rsc/dist/transforms/index.js:284-356removes non-export declarations, so the real function isn't reachable from the SSR-transformed module.Why neither env works
react-dom/server.edge'use server'direct callreact-servercondition picks throwing file; aliasing only the entrypoint isn't enough because nestedrequire('react')still resolves to thereact-servervariantregisterServerReferencekeeps the real function callablereact-serverconditioncreateServerReferencereturns a stub;callServerisnullRoute handlers are server-only — the client→server bridge is irrelevant for them, but vinext is forced to pick one of two envs that each break a real-world case.
Suggested fixes (in increasing order of invasiveness)
'use server'is unsupported in route-handler-imported files when using vinext, and recommend dropping the directive for functions only called server-side. (Mechanical fix; works today.)'use server'files. This needs a plugin-rsc change, not a vinext change.react-servercondition nor plugin-rsc's SSR'use server'transform. App Route handlers load from there. Both cases work.(3) feels like the architecturally correct answer — route handlers are a distinct execution context that doesn't fit cleanly in either RSC (render) or SSR (decode-and-render) semantics.
Environment
viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] }