Skip to content

client-{in-server-,}package-proxy load handlers crash under specific runtimes - two related failure modes #1207

@eashish93

Description

@eashish93

Related plugins

Describe the bug

@vitejs/plugin-rsc emits two flavors of "client reference proxy" virtual modules in dev: client-in-server-package-proxy (when a server-env file imports a package file via a relative or absolute path) and client-package-proxy (when a server-env file imports via a bare specifier). Both load handlers crash for us under the @cloudflare/sandbox SDK invocation path on first paint of the page, with two distinct failure modes depending on which one is hit. In normal vite dev, optimizeDeps rewrites the imports and masks both — under cf-sandbox SDK, the cache misses and the bugs surface.

Filed here at the maintainer's suggestion (cloudflare/vinext#1031 comment).

Bug 1 — client-in-server-package-proxy embeds absolute filesystem paths verbatim

Source location

packages/plugin-rsc/src/.../plugins.ts (bundled at dist/plugin-*.js). The branching logic that picks which proxy URL to emit:

const packageSource = packageSources.get(id);
if (!packageSource && this.environment.mode === "dev" && id.includes("/node_modules/")) {
  // CASE A — file is under node_modules but its import wasn't a bare specifier
  importId = `/@id/__x00__virtual:vite-rsc/client-in-server-package-proxy/${encodeURIComponent(id)}`;
} else if (packageSource) {
  if (this.environment.mode === "dev") {
    // CASE B — file's import WAS a bare specifier (recorded by the resolveId hook)
    importId = `/@id/__x00__virtual:vite-rsc/client-package-proxy/${packageSource}`;
  }
  // …
} else if (this.environment.mode === "dev") {
  // CASE C — file is in user code (outside node_modules)
  importId = normalizeViteImportAnalysisUrl(manager.server.environments[browserEnvironmentName], id);
}

CASE A's load handler:

id = decodeURIComponent(id.slice(49));
return `
  export * from ${JSON.stringify(id)};
  import * as __all__ from ${JSON.stringify(id)};
  export default __all__.default;
`;

The decoded id is the absolute filesystem path of the package file. Embedded as a string literal in the module body.

Symptom

[plugin:vite:import-analysis] Failed to resolve import
"/workspace/node_modules/<pkg>/dist/shims/X.js"
from "virtual:vite-rsc/client-in-server-package-proxy/..."

Root cause

Vite treats string-literal imports beginning with / as URL-relative-to-server-root, not filesystem-absolute. With root = /workspace, the import resolution looks up <root>/workspace/foo = /workspace/workspace/foo and fails. Triggers for any project root that is a path prefix of the imported file.

Suggested fix

Three angles, in increasing scope:

  1. Wrap the embedded path in /@fs/ form. Vite already treats /@fs/<absolute-path> as filesystem-absolute and skips URL-relative interpretation. One-line change.
  2. Resolve absolute path → bare specifier in CASE A before deciding URL form. Walk to nearest package.json, derive <name>/<subpath>, route through CASE B's existing path with that synthetic packageSource. Fixes the underlying mismatch.
  3. Run normalizeViteImportAnalysisUrl in CASE A (the same helper CASE C uses). Unifies the three branches.

Option 1 is the lowest-risk one-liner. Option 2 is more correct.


Bug 2 — client-package-proxy crashes when clientReferenceMetaMap lookup misses

Source location

client-package-proxy's load handler (sibling to CASE B above):

load: {
  filter: { id: prefixRegex("\0virtual:vite-rsc/client-package-proxy/") },
  async handler(id) {
    if (id.startsWith("\0virtual:vite-rsc/client-package-proxy/")) {
      assert(this.environment.mode === "dev");
      const source = id.slice(39);
      return `export {${
        Object.values(manager.clientReferenceMetaMap).find((v) => v.packageSource === source).exportNames.join(",")
      }} from ${JSON.stringify(source)};\n`;
    }
  }
}

The find(...) returns undefined when the rsc env hasn't yet transformed the package's file — the lookup runs before clientReferenceMetaMap is populated for this packageSource.

Symptom

TypeError: Cannot read properties of undefined (reading 'exportNames')
  at LoadPluginContext.handler (.../@vitejs/plugin-rsc/dist/plugin-*.js)
  at EnvironmentPluginContainer.load (.../vite/dist/node/chunks/node.js)

Fires on first paint when the proxy URL is fetched before the corresponding source file has been transformed in the rsc env.

Root cause

Race between two passes in plugin-rsc:

  1. Pass A (server env transforms a file with 'use client' boundary): populates clientReferenceMetaMap[id] with packageSource + exportNames.
  2. Pass B (proxy URL fetched, load handler runs): reads clientReferenceMetaMap to resolve which exports to forward.

Pass B can land before Pass A under certain runtimes (we observe this consistently under @cloudflare/sandbox SDK's containerFetch invocation path). Tracked previously at cloudflare/vinext#1008 — claim of fix in vinext 0.0.46 doesn't hold in our setup.

Suggested fix

Make the load handler defensive: when the metadata isn't there yet, fall back to wildcard re-export instead of throwing.

const meta = Object.values(manager.clientReferenceMetaMap).find((v) => v.packageSource === source);
return meta
  ? `export {${meta.exportNames.join(",")}} from ${JSON.stringify(source)};\n`
  : `export * from ${JSON.stringify(source)};\n`;

Wildcard loses the explicit-named-export signal but doesn't crash; subsequent requests after Pass A completes will use the precise form. Cleaner alternative: have the load handler await Pass A's completion (e.g. by triggering the source file's transform on demand if metadata is missing).


How they relate

CASE A and the bare-specifier path (CASE B → client-package-proxy) are alternative routes the same 'use client' boundary can take. Whether a package file ends up in CASE A or CASE B comes down to whether the FIRST import that reached it was a bare specifier or a relative path. Both paths have failure modes; consumers (vinext, ourselves) currently work around CASE A by rewriting all relative shim imports to bare specifiers, and around CASE B by patching the load handler to fall back on miss.

Both fixes belong in plugin-rsc rather than in every consuming package.

Reproduce

CodeSandbox / StackBlitz can't host this — Docker-based runtime is required (bug only surfaces under cf-sandbox SDK's containerFetch invocation; vite's optimizeDeps cache masks both bugs in any direct dev-server run). Standalone Docker repro is the smallest form available.

Repro repo: https://github.com/eashish93/vite-plugin-rsc-bug (also attached as zip at cloudflare/vinext#1031).

git clone https://github.com/eashish93/vite-plugin-rsc-bug
cd vinext-shim-bug-repro/main
bun install
bun run dev
# open http://localhost:5200/ → click "Start sandbox"

Without any workaround applied: Bug 1 fires on first paint. With Bug 1 patched (rewrite relative imports to bare specifiers): Bug 2 fires instead. Both need to be addressed for the page to render under cf-sandbox SDK.

Versions

Workarounds currently in production

  • Bug 1: post-install rewrite of ../shims/X.jsvinext/shims/X in node_modules/vinext/dist/**/*.js.
  • Bug 2: post-install patch of node_modules/@vitejs/plugin-rsc/dist/plugin-*.js to make the client-package-proxy load handler fall back to export * when clientReferenceMetaMap lookup misses.

Both scripts are small, idempotent, and re-applied on every bun install via postinstall.

Reproduction

https://github.com/eashish93/vite-plugin-rsc-bug

Steps to reproduce

No response

System Info

## System

- **OS:** macOS 26.3.1 (arm64 / Apple Silicon)
- **Runtime:** Node v25.2.1, Bun 1.3.11
- **Container runtime:** Docker 29.4.0 via OrbStack 2.1.1
- **Browser:** _(your browser + version — I don't have it)_

## Packages

| | |
|---|---|
| `vite` | 8.0.10 |
| `@vitejs/plugin-rsc` | 0.5.25 |
| `@vitejs/plugin-react` | 6.0.1 |
| `@cloudflare/vite-plugin` | 1.35.0 |
| `@cloudflare/sandbox` | 0.9.2 |
| `vinext` | 0.0.46 |
| `wrangler` | 4.87.0 |

Used Package Manager

bun

Logs

No response

Validations

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions