Skip to content

Commit

Permalink
Add stack trace to client rendering bailout error
Browse files Browse the repository at this point in the history
  • Loading branch information
huozhi committed Jan 26, 2024
1 parent 7a516d4 commit 0731c43
Show file tree
Hide file tree
Showing 8 changed files with 59 additions and 30 deletions.
16 changes: 16 additions & 0 deletions packages/next/src/lib/format-server-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ function setMessage(error: Error, message: string): void {
}
}

/**
* Input:
* Error: Something went wrong
at funcName (/path/to/file.js:10:5)
at anotherFunc (/path/to/file.js:15:10)
* Output:
at funcName (/path/to/file.js:10:5)
at anotherFunc (/path/to/file.js:15:10)
*/
export function getStackWithoutErrorMessage(error: Error): string {
const stack = error.stack
if (!stack) return ''
return stack.replace(/^[^\n]*\n/, '')
}

export function formatServerError(error: Error): void {
if (typeof error?.message !== 'string') return

Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import { isDynamicServerError } from '../../client/components/hooks-server-conte
import { useFlightResponse } from './use-flight-response'
import { isStaticGenBailoutError } from '../../client/components/static-generation-bailout'
import { isInterceptionRouteAppPath } from '../future/helpers/interception-routes'
import { getStackWithoutErrorMessage } from '../../lib/format-server-error'

export type GetDynamicParamFromSegment = (
// [slug] / [[slug]] / [...slug]
Expand Down Expand Up @@ -1014,8 +1015,9 @@ async function renderToHTMLOrFlightImpl(
console.log()

if (renderOpts.experimental.missingSuspenseWithCSRBailout) {
const stack = getStackWithoutErrorMessage(err)
error(
`${err.reason} should be wrapped in a suspense boundary at page "${pagePath}". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout`
`${err.reason} should be wrapped in a suspense boundary at page "${pagePath}". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout\n${stack}`
)

throw err
Expand Down
3 changes: 1 addition & 2 deletions packages/next/src/server/app-render/create-error-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ export function createErrorHandler({
const { logAppDirError } =
require('../dev/log-app-dir-error') as typeof import('../dev/log-app-dir-error')
logAppDirError(err)
}
if (process.env.NODE_ENV === 'production') {
} else {
console.error(err)
}
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Suspense } from 'react'

export default function Layout({ children }) {
return (
<html>
<head />
<body>
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { Suspense } from 'react'

export default function Layout({ children }) {
return (
<html>
<head />
<body>
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
</body>
<body>{children}</body>
</html>
)
}
13 changes: 11 additions & 2 deletions test/e2e/app-dir/missing-suspense-with-csr-bailout/app/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@

import { useSearchParams } from 'next/navigation'

export default function Page() {
function SearchParams() {
useSearchParams()
return <div>Page</div>
return null
}

export default function Page() {
return (
<>
<SearchParams />
<div>Page</div>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,36 @@ createNextDescribe(
describe('useSearchParams', () => {
const message = `useSearchParams() should be wrapped in a suspense boundary at page "/".`

it('should fail build if useSearchParams is not wrapped in a suspense boundary', async () => {
const { exitCode } = await next.build()
expect(exitCode).toBe(1)
expect(next.cliOutput).toContain(message)
// Can show the trace where the searchParams hook is used
expect(next.cliOutput).toMatch(/at.*server[\\/]app[\\/]page.js/)
})

it('should pass build if useSearchParams is wrapped in a suspense boundary', async () => {
await next.renameFile('app/layout.js', 'app/layout-no-suspense.js')
await next.renameFile('app/layout-suspense.js', 'app/layout.js')

await expect(next.build()).resolves.toEqual({
exitCode: 0,
cliOutput: expect.not.stringContaining(message),
})
})

it('should fail build if useSearchParams is not wrapped in a suspense boundary', async () => {
await next.renameFile('app/layout.js', 'app/layout-suspense.js')
await next.renameFile('app/layout-no-suspense.js', 'app/layout.js')

await expect(next.build()).resolves.toEqual({
exitCode: 1,
cliOutput: expect.stringContaining(message),
})

await next.renameFile('app/layout.js', 'app/layout-no-suspense.js')
await next.renameFile('app/layout-suspense.js', 'app/layout.js')
})
})

describe('next/dynamic', () => {
beforeEach(async () => {
await next.renameFile('app/page.js', 'app/_page.js')
await next.start()
})
afterEach(async () => {
await next.renameFile('app/_page.js', 'app/page.js')
})

it('does not emit errors related to bailing out of client side rendering', async () => {
const browser = await next.browser('/dynamic', {
Expand All @@ -53,8 +58,6 @@ createNextDescribe(
try {
await browser.waitForElementByCss('#dynamic')

// await new Promise((resolve) => setTimeout(resolve, 1000))

expect(await browser.log()).not.toContainEqual(
expect.objectContaining({
source: 'error',
Expand Down

0 comments on commit 0731c43

Please sign in to comment.