-
Notifications
You must be signed in to change notification settings - Fork 29.3k
Fix: Keep metadata outlet consistent in prerender and resume #82540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
Changes from 8 commits
3981ec3
a506827
d223dd8
040cd09
0c0d3a2
2b96873
2875e12
63c36bf
a1ddf52
9a97121
e3a2efd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -420,11 +420,13 @@ async function createComponentTreeInternal( | |||||||||||||
? process.env.__NEXT_EDGE_PROJECT_DIR | ||||||||||||||
: ctx.renderOpts.dir) || '' | ||||||||||||||
|
||||||||||||||
// Use the same condition to render metadataOutlet as metadata | ||||||||||||||
const metadataOutlet = StreamingMetadataOutlet ? ( | ||||||||||||||
<StreamingMetadataOutlet /> | ||||||||||||||
) : ( | ||||||||||||||
<MetadataOutlet ready={getMetadataReady} /> | ||||||||||||||
// StreamingMetadataOutlet is now always null from createMetadataComponents | ||||||||||||||
// to ensure consistency between prerender and resume for PPR routes | ||||||||||||||
const metadataOutlet = ( | ||||||||||||||
<MetadataOutlet | ||||||||||||||
ready={getMetadataReady} | ||||||||||||||
StreamingComponent={StreamingMetadataOutlet} | ||||||||||||||
/> | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
const [notFoundElement, notFoundFilePath] = | ||||||||||||||
|
@@ -823,10 +825,12 @@ async function createComponentTreeInternal( | |||||||||||||
<React.Fragment key={cacheNodeKey}> | ||||||||||||||
{wrappedPageElement} | ||||||||||||||
{layerAssets} | ||||||||||||||
{/* TEST: Commenting out metadata outlets to verify error source | ||||||||||||||
<OutletBoundary> | ||||||||||||||
<MetadataOutlet ready={getViewportReady} /> | ||||||||||||||
{metadataOutlet} | ||||||||||||||
</OutletBoundary> | ||||||||||||||
*/} | ||||||||||||||
Comment on lines
+828
to
+833
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The metadata outlet error boundaries have been completely commented out, which means metadata-related errors will not be properly caught and handled during rendering. View DetailsAnalysisThe code has commented out the entire {/* TEST: Commenting out metadata outlets to verify error source
<OutletBoundary>
<MetadataOutlet ready={getViewportReady} />
{metadataOutlet}
</OutletBoundary>
*/} This removes critical error handling for metadata operations. The
RecommendationRestore the |
||||||||||||||
</React.Fragment>, | ||||||||||||||
parallelRouteCacheNodeSeedData, | ||||||||||||||
loadingData, | ||||||||||||||
|
@@ -1014,9 +1018,15 @@ async function createComponentTreeInternal( | |||||||||||||
|
||||||||||||||
async function MetadataOutlet({ | ||||||||||||||
ready, | ||||||||||||||
StreamingComponent, | ||||||||||||||
}: { | ||||||||||||||
ready: () => Promise<void> & { status?: string; value?: unknown } | ||||||||||||||
StreamingComponent?: React.ComponentType | null | ||||||||||||||
}) { | ||||||||||||||
if (StreamingComponent) { | ||||||||||||||
return <StreamingComponent /> | ||||||||||||||
} | ||||||||||||||
|
||||||||||||||
const r = ready() | ||||||||||||||
// We can avoid a extra microtask by unwrapping the instrumented promise directly if available. | ||||||||||||||
if (r.status === 'rejected') { | ||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Default() { | ||
return <div>Content</div> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Suspense } from 'react' | ||
|
||
async function AsyncComponent() { | ||
await new Promise((resolve) => setTimeout(resolve, 100)) | ||
return <div>Async content {Math.random()}</div> | ||
} | ||
|
||
export default function Page() { | ||
return ( | ||
<Suspense fallback={<div>Loading...</div>}> | ||
<AsyncComponent /> | ||
</Suspense> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Default() { | ||
return null | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { headers } from 'next/headers' | ||
import { Metadata } from 'next' | ||
import { unstable_noStore } from 'next/cache' | ||
|
||
async function getChatIdFromHeaders() { | ||
const headersList = await headers() | ||
return headersList.get('x-chat-id') || 'default-id' | ||
} | ||
|
||
export async function generateMetadata(): Promise<Metadata> { | ||
const chatId = await getChatIdFromHeaders() | ||
|
||
// Simulate async DB call like v0 | ||
await new Promise((resolve) => setTimeout(resolve, 100)) | ||
|
||
return { | ||
title: `Chat ${chatId}`, | ||
description: `Chat session ${chatId}`, | ||
} | ||
} | ||
|
||
export default async function ChatPage() { | ||
unstable_noStore() | ||
|
||
const chatId = await getChatIdFromHeaders() | ||
|
||
if (!chatId) { | ||
return <div>No chat ID</div> | ||
} | ||
|
||
// Simulate some async work | ||
await new Promise((resolve) => setTimeout(resolve, 50)) | ||
const random = Math.random() | ||
|
||
return ( | ||
<div> | ||
<h1>Chat {chatId}</h1> | ||
<p>Random: {random}</p> | ||
</div> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Default() { | ||
return null | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function Default() { | ||
return null | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Suspense } from 'react' | ||
|
||
export default async function Layout({ | ||
children, | ||
content, | ||
}: { | ||
children: React.ReactNode | ||
content: React.ReactNode | ||
params: Promise<{ variants: string }> | ||
}) { | ||
return ( | ||
<> | ||
{children} | ||
<Suspense>{content}</Suspense> | ||
</> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { ReactNode, Suspense } from 'react' | ||
export default function Root({ | ||
children, | ||
sidebar, | ||
content, | ||
}: { | ||
children: ReactNode | ||
sidebar: ReactNode | ||
content: ReactNode | ||
}) { | ||
return ( | ||
<html> | ||
<body> | ||
<Suspense> | ||
{sidebar} | ||
{children} | ||
{content} | ||
</Suspense> | ||
</body> | ||
</html> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
export default async function Page({ | ||
searchParams, | ||
}: { | ||
searchParams: Promise<{ id: string | undefined }> | ||
}) { | ||
const { id } = await searchParams | ||
const random = Math.random() | ||
return ( | ||
<p> | ||
hello world {random} {id} | ||
</p> | ||
) | ||
} | ||
|
||
export const generateMetadata = async () => { | ||
return { | ||
title: 'Hello World', | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export default async function Slot1() { | ||
await new Promise((resolve) => setTimeout(resolve, 10)) | ||
return <div>Slot 1</div> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export default async function Slot2() { | ||
await new Promise((resolve) => setTimeout(resolve, 20)) | ||
return <div>Slot 2</div> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export default async function Slot3() { | ||
await new Promise((resolve) => setTimeout(resolve, 30)) | ||
return <div>Slot 3</div> | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
serveStreamingMetadata
logic has been hardcoded totrue
, bypassing user agent detection and bot handling, which could cause incorrect behavior for different client types.View Details
📝 Patch Details
Analysis
The code has hardcoded
serveStreamingMetadata = true
and commented out the original logic that determined this value based on user agent detection and bot handling:This bypasses important logic that:
htmlLimitedBots
configurationAdditionally, the bot-specific logic
serveStreamingMetadata = false
for HTML bots with PPR is also commented out, which could cause bots to receive streaming content when they should receive static content.Recommendation
Restore the original user agent detection logic and bot handling. If there are specific issues with the existing logic, they should be fixed properly rather than bypassed with hardcoded values. The changes should respect the existing API contracts and configuration options.