Skip to content

App Route handlers can't both render react-dom/server.edge and call 'use server' functions (RSC vs SSR env conflict) #1246

@eashish93

Description

@eashish93

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)

  1. 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.)
  2. 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.
  3. 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'] }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions