Skip to content

Commit

Permalink
Revert "if there are errors during postpone, or postpone was caught, …
Browse files Browse the repository at this point in the history
…fail static generation (#57477)"

This reverts commit 2c21635.
  • Loading branch information
ztanner committed Oct 26, 2023
1 parent 07483d4 commit 138fb4b
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 114 deletions.
163 changes: 93 additions & 70 deletions packages/next/src/export/routes/app-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ 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 @@ -79,35 +80,8 @@ export async function exportAppPage(
pathname = '/404'
}

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) {
try {
if (isAppPrefetch) {
await generatePrefetchRsc(
req,
path,
Expand All @@ -117,60 +91,109 @@ export async function exportAppPage(
renderOpts,
fileWriter
)

return { revalidate: 0 }
}

const { staticBailoutInfo = {} } = metadata
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 (revalidate === 0 && debugOutput && staticBailoutInfo?.description) {
const err = new Error(
`Static generation failed due to dynamic usage on ${path}, reason: ${staticBailoutInfo.description}`
)
if (!(renderOpts as any).store.staticPrefetchBailout) {
await generatePrefetchRsc(
req,
path,
res,
pathname,
htmlFilepath,
renderOpts,
fileWriter
)
}

// 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'))
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}`
)

// 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'))
}

console.warn(err)
}

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

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

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

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

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

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

// Writing the RSC payload to a file.
await fileWriter(
ExportedAppPageFiles.FLIGHT,
htmlFilepath.replace(/\.html$/, '.rsc'),
flightData
)
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
}

return {
// Only include the metadata if the environment has next support.
metadata: hasNextSupport ? meta : undefined,
hasEmptyPrelude: Boolean(postponed) && html === '',
hasPostponed: Boolean(postponed),
revalidate,
return { revalidate: 0, hasEmptyPrelude: true }
}
}
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 '../../server/app-render/is-dynamic-usage-error'
import { isDynamicUsageError } from '../helpers/is-dynamic-usage-error'
import { SERVER_DIRECTORY } from '../../shared/lib/constants'
import { hasNextSupport } from '../../telemetry/ci-info'

Expand Down
33 changes: 20 additions & 13 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 } from '../../build/output/log'
import { warn, error } 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,8 +72,6 @@ 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 @@ -1001,25 +999,34 @@ 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
) {
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`
warn('')
warn(
`${urlPathname} opted out of partial prerendering because the postpone signal was intercepted by a try/catch in your application code.`
)
}

for (const err of capturedErrors) {
if (!isDynamicUsageError(err) && !isPostpone(err)) {
throw err
}

if (isDynamicUsageError(err)) {
staticGenerationStore.revalidate = 0
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 ')}`)
}
}
}
// 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) {
staticGenerationStore.revalidate = 0
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/server/app-render/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export interface RenderOptsPartial {
isPrefetch?: boolean
ppr: boolean
postponed?: string
hasPostponeErrors?: boolean
}

export type RenderOpts = LoadComponentsReturnType<AppPageModule> &
Expand Down
9 changes: 0 additions & 9 deletions test/e2e/app-dir/ppr-errors/app/layout.jsx

This file was deleted.

10 changes: 0 additions & 10 deletions test/e2e/app-dir/ppr-errors/next.config.js

This file was deleted.

10 changes: 0 additions & 10 deletions test/e2e/app-dir/ppr-errors/ppr.test.ts

This file was deleted.

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.
in a try/catch. This test does not re-throw the caught error.
</p>
<div id="container">
<Suspense fallback={<div>Loading...</div>}>
Expand Down
34 changes: 34 additions & 0 deletions test/e2e/app-dir/ppr/app/suspense/node/cookies-error/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React, { Suspense } from 'react'
import { cookies } from 'next/headers'

import { Dynamic } from '../../../../components/dynamic'

export default async function Page() {
return (
<>
<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 re-throws the error that is caught.
</p>
<div id="container">
<Suspense fallback={<Dynamic fallback />}>
<Dynamic catchErrors />
</Suspense>

<Suspense fallback={<div>Loading...</div>}>
<Foobar />
</Suspense>
</div>
</>
)
}

async function Foobar() {
try {
cookies()
} catch (err) {
throw new Error('You are not signed in')
}
return null
}
33 changes: 33 additions & 0 deletions test/e2e/app-dir/ppr/app/suspense/node/fetch-error/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { Suspense } from 'react'

import { Dynamic } from '../../../../components/dynamic'

export default async function Page() {
const getData = () =>
fetch('https://example.vercel.sh', {
cache: 'no-store',
})
.then((res) => res.text())
.then((text) => new Promise((res) => setTimeout(() => res(text), 1000)))

try {
await getData()
} catch (err) {
throw new Error('Fetch failed')
}

return (
<>
<h2>Dynamic Component Catching Errors</h2>
<p>
This shows the dynamic component that reads cookies but wraps the read
in a try/catch.
</p>
<div id="container">
<Suspense fallback={<Dynamic fallback />}>
<Dynamic catchErrors />
</Suspense>
</div>
</>
)
}
Loading

0 comments on commit 138fb4b

Please sign in to comment.