cloudflare / cloudflare/vinext

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

Open
#1,246 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 6h
Merged PRs (30d)
120

Description

## 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:

```ts
// 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
...
}
```

```ts
// lib/actions/accounts/createAccount.ts
'use server';
export async function createAccount(decoded: DecodedIdToken) { ... }
```

```ts
// 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'] }`

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.