fix: normalize null-prototype params from matchPattern() for RSC serialization#243
Conversation
…alization
matchPattern() uses Object.create(null), producing objects without
Object.prototype. The RSC serializer rejects these during dynamic route
rendering, causing 500 errors.
The fix spreads params ({...obj}) to restore a normal prototype, then
assigns properties onto the Promise to preserve synchronous access
(params.id, params.slug) for pre-Next.js 15 code. This satisfies both
constraints:
1. RSC serializer accepts objects with Object.prototype
2. Existing components can access params synchronously without await
Fixes cloudflare#241
|
/bigbonk review please |
1 similar comment
|
/bigbonk review please |
commit: |
There was a problem hiding this comment.
Clean, well-scoped fix. The approach is correct: matchPattern() uses Object.create(null), and spreading into {...obj} restores a normal prototype so the RSC serializer accepts it. The Object.assign onto the Promise preserves backward compat for synchronous params.id access.
I verified:
- All 6 call sites in
app-dev-server.tsand 3 inmetadata.tsxare updated - Intercepting route params also flow through
makeThenableParams(viaslotParamsat lines 801/809) - The
__matchConfigPatternfunction also usesObject.create(null)but its params are only used for redirect/rewrite destination interpolation, not RSC serialization, so it's unaffected - No production server or worker entry parity concerns — the App Router prod server delegates to the built RSC entry, so it inherits this fix automatically
searchParams(spObj) is constructed with{}(normal prototype), somakeThenableParamson it is a no-op prototype-wise but keeps the code consistent
Two minor suggestions below, neither blocking.
| * Normalize null-prototype objects from matchPattern() into thenable objects. | ||
| * See app-dev-server.ts makeThenableParams() for full explanation. | ||
| */ | ||
| function makeThenableParams<T extends Record<string, unknown>>(obj: T): Promise<T> & T { |
There was a problem hiding this comment.
Nit: this is a private copy of the same function that exists in app-dev-server.ts. If metadata.tsx ever gets more call sites, or if the logic needs to change (e.g. adding Object.freeze for immutability), both copies need to stay in sync.
Worth considering exporting it from a shared utility module (e.g. packages/vinext/src/utils/thenable-params.ts) and importing in both files. Not blocking — the duplication is small and the function is unlikely to change frequently.
| // restores a normal prototype. Object.assign onto the Promise preserves | ||
| // synchronous property access (params.id, params.slug) that existing | ||
| // components and test fixtures rely on. | ||
| function makeThenableParams(obj) { |
There was a problem hiding this comment.
Nit: the function in metadata.tsx is typed (<T extends Record<string, unknown>>), but this one is untyped since this file is generated code emitted as a string. That's fine — just noting for future reference that if someone wants to add type safety to the generated RSC entry, the metadata version is the canonical typed signature.
|
Review submitted. Here's a summary: Approved the PR. The fix is correct and well-scoped:
I left two non-blocking nits: (1) the duplicated |
Summary
matchPattern()usesObject.create(null), producing objects withoutObject.prototype. The RSC serializer rejects these during dynamic route rendering, causing 500 errors on all dynamic routes (e.g./dashboards/[id]).Fixes #241
Fix
A
makeThenableParams()helper that satisfies both constraints:{...obj}) normalizes the null prototype → RSC serializer accepts itObject.assignonto the Promise preserves synchronous property access (params.id,params.slug) for pre-Next.js 15 codeThis replaces the previous
Object.assign(Promise.resolve(params), params)pattern at all 6 call sites inapp-dev-server.tsand 3 inmetadata.tsx. The old pattern passed the original null-prototype object to bothPromise.resolve()andObject.assign()— while the thenable worked, the resolved value still had a null prototype causing RSC serialization failure.Files Changed (2 files, 1 commit)
packages/vinext/src/server/app-dev-server.tsmakeThenableParams(), replaced 6Object.assign(Promise.resolve(x), x)sites, updated stale commentpackages/vinext/src/shims/metadata.tsxmakeThenableParams<T>(), replaced 3 call sites, updated commentTesting
params.id,params.slug, catch-all routes (params.slug.join()),generateMetadata()with async params — all passvs. PR #242
This is a replacement for #242, which was correctly identified by review as breaking synchronous params access. Changes from #242:
Promise.resolve({...obj})→Object.assign(Promise.resolve({...obj}), {...obj})— preserves backward compatcredentials: "same-origin"changes (4 fetch calls)findReactServerPackages()feature.gitignorechangelink.tsx/navigation.tschanges