Skip to content
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

if there are errors during postpone, or postpone was caught, fail static generation #57477

Merged
merged 6 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 70 additions & 93 deletions packages/next/src/export/routes/app-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
NEXT_URL,
NEXT_ROUTER_PREFETCH,
} from '../../client/components/app-router-headers'
import { isDynamicUsageError } from '../helpers/is-dynamic-usage-error'
import { NEXT_CACHE_TAGS_HEADER } from '../../lib/constants'
import { hasNextSupport } from '../../telemetry/ci-info'
import { lazyRenderAppPage } from '../../server/future/route-modules/app-page/module.render'
Expand Down Expand Up @@ -80,8 +79,35 @@ export async function exportAppPage(
pathname = '/404'
}

try {
if (isAppPrefetch) {
if (isAppPrefetch) {
await generatePrefetchRsc(
req,
path,
res,
pathname,
htmlFilepath,
renderOpts,
fileWriter
)

return { revalidate: 0 }
}

const result = await lazyRenderAppPage(req, res, pathname, query, renderOpts)
const html = result.toUnchunkedString()
const { metadata } = result
const flightData = metadata.pageData
const revalidate = metadata.revalidate ?? false
const postponed = metadata.postponed

if (revalidate === 0) {
if (isDynamicError) {
throw new Error(
`Page with dynamic = "error" encountered dynamic data method on ${path}.`
)
}

if (!(renderOpts as any).store.staticPrefetchBailout) {
await generatePrefetchRsc(
req,
path,
Expand All @@ -91,109 +117,60 @@ export async function exportAppPage(
renderOpts,
fileWriter
)

return { revalidate: 0 }
}

const result = await lazyRenderAppPage(
req,
res,
pathname,
query,
renderOpts
)
const html = result.toUnchunkedString()
const { metadata } = result
const flightData = metadata.pageData
const revalidate = metadata.revalidate ?? false
const postponed = metadata.postponed

if (revalidate === 0) {
if (isDynamicError) {
throw new Error(
`Page with dynamic = "error" encountered dynamic data method on ${path}.`
)
}

if (!(renderOpts as any).store.staticPrefetchBailout) {
await generatePrefetchRsc(
req,
path,
res,
pathname,
htmlFilepath,
renderOpts,
fileWriter
)
}

const { staticBailoutInfo = {} } = metadata

if (revalidate === 0 && debugOutput && staticBailoutInfo?.description) {
const err = new Error(
`Static generation failed due to dynamic usage on ${path}, reason: ${staticBailoutInfo.description}`
)
const { staticBailoutInfo = {} } = metadata

// Update the stack if it was provided via the bailout info.
const { stack } = staticBailoutInfo
if (stack) {
err.stack = err.message + stack.substring(stack.indexOf('\n'))
}
if (revalidate === 0 && debugOutput && staticBailoutInfo?.description) {
const err = new Error(
`Static generation failed due to dynamic usage on ${path}, reason: ${staticBailoutInfo.description}`
)

console.warn(err)
// Update the stack if it was provided via the bailout info.
const { stack } = staticBailoutInfo
if (stack) {
err.stack = err.message + stack.substring(stack.indexOf('\n'))
}

return { revalidate: 0 }
console.warn(err)
}

let headers: OutgoingHttpHeaders | undefined
if (metadata.fetchTags) {
headers = { [NEXT_CACHE_TAGS_HEADER]: metadata.fetchTags }
}
return { revalidate: 0 }
}

// Writing static HTML to a file.
await fileWriter(
ExportedAppPageFiles.HTML,
htmlFilepath,
html ?? '',
'utf8'
)
let headers: OutgoingHttpHeaders | undefined
if (metadata.fetchTags) {
headers = { [NEXT_CACHE_TAGS_HEADER]: metadata.fetchTags }
}

// Writing the request metadata to a file.
const meta: RouteMetadata = {
status: undefined,
headers,
postponed,
}
// Writing static HTML to a file.
await fileWriter(ExportedAppPageFiles.HTML, htmlFilepath, html ?? '', 'utf8')

await fileWriter(
ExportedAppPageFiles.META,
htmlFilepath.replace(/\.html$/, '.meta'),
JSON.stringify(meta, null, 2)
)
// Writing the request metadata to a file.
const meta: RouteMetadata = {
status: undefined,
headers,
postponed,
}

// Writing the RSC payload to a file.
await fileWriter(
ExportedAppPageFiles.FLIGHT,
htmlFilepath.replace(/\.html$/, '.rsc'),
flightData
)
await fileWriter(
ExportedAppPageFiles.META,
htmlFilepath.replace(/\.html$/, '.meta'),
JSON.stringify(meta, null, 2)
)

return {
// Only include the metadata if the environment has next support.
metadata: hasNextSupport ? meta : undefined,
hasEmptyPrelude: Boolean(postponed) && html === '',
hasPostponed: Boolean(postponed),
revalidate,
}
} catch (err: any) {
// if the error isn't a special dynamic usage error (caught by Next)
// we also do not throw the error if it occurred while attempting a postpone
// since those will be captured and logged during build/ISR
if (!isDynamicUsageError(err) && !renderOpts.hasPostponeErrors) {
throw err
}
// Writing the RSC payload to a file.
await fileWriter(
ExportedAppPageFiles.FLIGHT,
htmlFilepath.replace(/\.html$/, '.rsc'),
flightData
)

return { revalidate: 0, hasEmptyPrelude: true }
return {
// Only include the metadata if the environment has next support.
metadata: hasNextSupport ? meta : undefined,
hasEmptyPrelude: Boolean(postponed) && html === '',
hasPostponed: Boolean(postponed),
revalidate,
}
}
2 changes: 1 addition & 1 deletion packages/next/src/export/routes/app-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type {
MockedRequest,
MockedResponse,
} from '../../server/lib/mock-request'
import { isDynamicUsageError } from '../helpers/is-dynamic-usage-error'
import { isDynamicUsageError } from '../../server/app-render/is-dynamic-usage-error'
import { SERVER_DIRECTORY } from '../../shared/lib/constants'
import { hasNextSupport } from '../../telemetry/ci-info'

Expand Down
33 changes: 13 additions & 20 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import { validateURL } from './validate-url'
import { createFlightRouterStateFromLoaderTree } from './create-flight-router-state-from-loader-tree'
import { handleAction } from './action-handler'
import { NEXT_DYNAMIC_NO_SSR_CODE } from '../../shared/lib/lazy-dynamic/no-ssr-error'
import { warn, error } from '../../build/output/log'
import { warn } from '../../build/output/log'
import { appendMutableCookies } from '../web/spec-extension/adapters/request-cookies'
import { createServerInsertedHTML } from './server-inserted-html'
import { getRequiredScripts } from './required-scripts'
Expand All @@ -72,6 +72,8 @@ import { createComponentTree } from './create-component-tree'
import { getAssetQueryString } from './get-asset-query-string'
import { setReferenceManifestsSingleton } from './action-encryption-utils'
import { createStaticRenderer } from './static/static-renderer'
import { isPostpone } from '../lib/router-utils/is-postpone'
import { isDynamicUsageError } from './is-dynamic-usage-error'

export type GetDynamicParamFromSegment = (
// [slug] / [[slug]] / [...slug]
Expand Down Expand Up @@ -999,33 +1001,24 @@ async function renderToHTMLOrFlightImpl(
if (staticGenerationStore.isStaticGeneration) {
const htmlResult = await renderResult.toUnchunkedString(true)

if (renderOpts.ppr && postponeErrors.length > 0) {
renderOpts.hasPostponeErrors = true
}

if (
renderOpts.ppr &&
staticGenerationStore.postponeWasTriggered &&
!extraRenderResultMeta.postponed
) {
warn('')
warn(
`${urlPathname} opted out of partial prerendering because the postpone signal was intercepted by a try/catch in your application code.`
throw new Error(
`Postpone signal was caught while rendering ${urlPathname}. These errors should not be caught during static generation. Learn more: https://nextjs.org/docs/messages/ppr-postpone-errors`
)
}

if (postponeErrors.length > 0) {
warn(
'The following errors were re-thrown, and might help find the location of the try/catch that triggered this.'
)
for (let i = 0; i < postponeErrors.length; i++) {
error(`${postponeErrors[i].stack?.split('\n').join('\n ')}`)
}
for (const err of capturedErrors) {
if (!isDynamicUsageError(err) && !isPostpone(err)) {
throw err
}

if (isDynamicUsageError(err)) {
staticGenerationStore.revalidate = 0
}
}
// if we encountered any unexpected errors during build
// we fail the prerendering phase and the build
if (capturedErrors.length > 0) {
throw capturedErrors[0]
}

if (staticGenerationStore.forceStatic === false) {
Expand Down
1 change: 0 additions & 1 deletion packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ export interface RenderOptsPartial {
isPrefetch?: boolean
ppr: boolean
postponed?: string
hasPostponeErrors?: boolean
}

export type RenderOpts = LoadComponentsReturnType<AppPageModule> &
Expand Down
9 changes: 9 additions & 0 deletions test/e2e/app-dir/ppr-errors/app/layout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'

export default function Root({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default async function Page() {
<h2>Dynamic Component Catching Errors</h2>
<p>
This shows the dynamic component that reads cookies but wraps the read
in a try/catch. This test does not re-throw the caught error.
in a try/catch.
</p>
<div id="container">
<Suspense fallback={<div>Loading...</div>}>
Expand Down
10 changes: 10 additions & 0 deletions test/e2e/app-dir/ppr-errors/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
experimental: {
ppr: true,
},
}

module.exports = nextConfig
10 changes: 10 additions & 0 deletions test/e2e/app-dir/ppr-errors/ppr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { nextBuild } from 'next-test-utils'

describe('ppr build errors', () => {
it('should fail the build', async () => {
const out = await nextBuild(__dirname, [], { stderr: true })
expect(out.stderr).toContain(
'Postpone signal was caught while rendering /. These errors should not be caught during static generation.'
)
})
})
34 changes: 0 additions & 34 deletions test/e2e/app-dir/ppr/app/suspense/node/cookies-error/page.jsx

This file was deleted.

33 changes: 0 additions & 33 deletions test/e2e/app-dir/ppr/app/suspense/node/fetch-error/page.jsx

This file was deleted.

Loading
Loading