mirror of
https://github.com/cloudflare/vinext.git
synced 2026-05-09 08:25:34 +02:00
[PR #250] [MERGED] fix(rsc): emit actionable hint when non-plain object crosses server-client boundary #418
Labels
No labels
enhancement
enhancement
good first issue
help wanted
nextjs-tracking
nextjs-tracking
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/vinext#418
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/cloudflare/vinext/pull/250
Author: @SeolJaeHyeok
Created: 3/4/2026
Status: ✅ Merged
Merged: 3/4/2026
Merged by: @southpolesteve
Base:
main← Head:fix/rsc-serialization-error-hint📝 Commits (3)
8fb2002fix(rsc): emit actionable hint when non-plain object crosses server-client boundary2908d79test(rsc): add unit tests for rscOnError non-plain object dev hint7ea4fcefix: escape newlines in rscOnError hint and add runtime tests📊 Changes
2 files changed (+156 additions, -1 deletions)
View changed files
📝
packages/vinext/src/server/app-dev-server.ts(+42 -0)📝
tests/app-router.test.ts(+114 -1)📄 Description
Fixes #237
Problem
When a Server Component passes a non-serializable value (class instance, ESM module namespace object,
Date,Map, orObject.create(null)) as a prop to a Client Component, React's RSC serializer throws:The error surfaces in the browser with no indication of which component or prop triggered it, making it nearly impossible to debug in large applications. Users migrating from plain Next.js are especially affected because the same code silently works there (see root cause below).
Root Cause
This is a fundamental difference between how Vite and webpack handle ES modules:
import * as X from './mod'{ __esModule: true, ...exports }— plain objectisPlainObjectcheck)[[Module]]slot)When users pass
import * as utilsas a prop to a Client Component, webpack wraps it as a plain__esModuleobject that React's RSC serializer accepts. Vite produces a real ESM Module Namespace Object with a null-like prototype that React correctly rejects. vinext's behavior is actually more spec-correct — Next.js/webpack was accidentally permitting invalid semantics.Solutions Considered
✅ Solution 1 — User-land fix (always applicable)
Convert non-serializable values to plain objects before passing them across
the RSC boundary:
✅ Solution 2 — Actionable dev-mode hint in rscOnError (this PR)
Enhanced rscOnError in app-dev-server.ts to detect this specific error
in dev mode and print a structured, actionable message to the server console:
No behavior change in production — the existing digest hash logic is unchanged.
❌ Solution 3 — Auto-convert Module Namespace Objects in vinext (not applied)
An alternative approach was considered: intercept props before
renderToReadableStream and automatically convert ESM Module Namespace
Objects to plain objects via { ...moduleNS } or
Object.fromEntries(Object.entries(moduleNS)).
This was rejected for three reasons:
important distinctions — live bindings become static snapshots, and any
non-enumerable exports or symbols are lost without warning. It would make
vinext behave like webpack rather than being correct.
object and passing it to a Client Component means all exports of that
module would be bundled into the client, defeating the RSC boundary's
purpose of keeping server-only code out of the client bundle.
non-serializable data across an RSC boundary. Silently fixing it
server-side masks the problem instead of helping the user understand and
fix their code. A clear error message is the correct resolution.
Changes
Testing
Minimal reproduction:
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
usePathname/useSearchParamsReact error) #505