Files
vercel__next.js/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts
Hendrik Liebau dae438dc0d Stop printing a stack frame for error message text (#97829)
Next.js printed a stack frame that does not exist for an error whose
message ends with text that looks like a file location. A logged
connection failure read `at <unknown> (Error: connect ECONNREFUSED
::1:45999)` in front of its real frames.

`stacktrace-parser` tries five patterns on every line, and the pattern
for JavaScriptCore makes the `<name>@` prefix optional. That pattern
`:<line>:<column>`, and a line of an error message can end that way. A
JavaScriptCore frame always holds `@`, also at the top level, so the
prefix is not optional in a real stack.

The patch for `stacktrace-parser` now requires that prefix. A line of a
message no longer becomes a frame, and every JavaScriptCore frame still
parses, including one whose path holds a space.

A Turbopack build error puts the file and the position of the error in
its message, and the parser read that line as a frame as well. That
frame is gone now, and the location stays in the message. The snapshots
of the build errors record the change.
2026-08-25 13:38:47 +02:00

1132 lines
49 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as path from 'path'
import { nextTestSetup } from 'e2e-utils'
import stripAnsi from 'strip-ansi'
import { retry } from 'next-test-utils'
function normalizeCliOutput(output: string) {
return (
stripAnsi(output)
// TODO(veil): Should not appear in sourcemapped stackframes.
.replaceAll('webpack:///', 'bundler:///')
.replaceAll(/at [a-zA-Z] \(/g, 'at <mangled> (')
)
}
describe('app-dir - server source maps', () => {
const dependencies = {
// `link:` simulates a package in a monorepo
'internal-pkg': `link:./internal-pkg`,
'external-pkg': `file:./external-pkg`,
}
const { skipped, next, isNextDev, isTurbopack, isRspack } = nextTestSetup({
dependencies,
files: path.join(__dirname, 'fixtures/default'),
// Deploy tests don't have access to runtime logs.
// Manually verify that the runtime logs match.
skipDeployment: true,
})
if (skipped) return
it('logged errors have a sourcemapped stack with a codeframe', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
await next.render('/rsc-error-log')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: rsc-error-log'
)
})
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'Error: rsc-error-log' +
'\n at logError (app/rsc-error-log/page.js:4:17)' +
'\n at Page (app/rsc-error-log/page.js:9:3)' +
'\n 2 |' +
'\n 3 | function logError() {' +
"\n> 4 | const error = new Error('rsc-error-log')" +
'\n | ^' +
'\n 5 | console.error(error)' +
'\n 6 | }' +
'\n 7 |' +
'\n'
)
} else {
if (isTurbopack) {
// TODO(veil): Sourcemap names
// TODO(veil): relative paths in webpack
expect(normalizeCliOutput(next.cliOutput)).toContain(
'(app/rsc-error-log/page.js:4:17)'
)
expect(normalizeCliOutput(next.cliOutput)).toContain(
'' +
"\n> 4 | const error = new Error('rsc-error-log')" +
'\n | ^'
)
} else {
// TODO(veil): line/column numbers are flaky in Webpack
}
}
})
it('logged errors have a sourcemapped `cause`', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
await next.render('/rsc-error-log-cause')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: rsc-error-log-cause'
)
})
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'Error: rsc-error-log-cause' +
'\n at logError (app/rsc-error-log-cause/page.js:2:17)' +
'\n at Page (app/rsc-error-log-cause/page.js:8:3)' +
'\n 1 | function logError(cause) {' +
"\n> 2 | const error = new Error('rsc-error-log-cause', { cause })" +
'\n | ^' +
'\n 3 | console.error(error)' +
'\n 4 | }' +
'\n 5 | {' +
'\n [cause]: Error: Boom' +
'\n at Page (app/rsc-error-log-cause/page.js:7:17)' +
'\n 5 |' +
'\n 6 | export default function Page() {' +
"\n > 7 | const error = new Error('Boom')" +
'\n | ^' +
'\n 8 | logError(error)' +
'\n 9 | return null' +
'\n 10 | }' +
'\n'
)
} else {
if (isTurbopack) {
// TODO(veil): Sourcemap names
// TODO(veil): relative paths
expect(normalizeCliOutput(next.cliOutput)).toContain(
'(app/rsc-error-log-cause/page.js:2:17)'
)
expect(normalizeCliOutput(next.cliOutput)).toContain(
'(app/rsc-error-log-cause/page.js:7:17)'
)
expect(normalizeCliOutput(next.cliOutput)).toContain(
'' +
"\n> 2 | const error = new Error('rsc-error-log-cause', { cause })" +
'\n | ^'
)
expect(normalizeCliOutput(next.cliOutput)).toContain(
'' +
"\n > 7 | const error = new Error('Boom')" +
'\n | ^'
)
} else {
// TODO(veil): line/column numbers are flaky in Webpack
}
}
})
it('logged errors collapse deeply nested causes at depth 2', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
await next.render('/rsc-error-log-nested')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: rsc-error-log-nested'
)
})
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'Error: rsc-error-log-nested' +
'\n at logError (app/rsc-error-log-nested/page.js:6:18)' +
'\n at Page (app/rsc-error-log-nested/page.js:11:3)' +
"\n 4 | const depth2 = new Error('Depth 2 error', { cause: depth3 })" +
"\n 5 | const depth1 = new Error('Depth 1 error', { cause: depth2 })" +
"\n> 6 | const depth0 = new Error('rsc-error-log-nested', { cause: depth1 })" +
'\n | ^' +
'\n 7 | console.error(depth0)' +
'\n 8 | }' +
'\n 9 | {' +
'\n [cause]: Error: Depth 1 error' +
'\n at logError (app/rsc-error-log-nested/page.js:5:18)' +
'\n at Page (app/rsc-error-log-nested/page.js:11:3)' +
"\n 3 | const depth3 = new Error('Depth 3 error', { cause: depth4 })" +
"\n 4 | const depth2 = new Error('Depth 2 error', { cause: depth3 })" +
"\n > 5 | const depth1 = new Error('Depth 1 error', { cause: depth2 })" +
'\n | ^' +
"\n 6 | const depth0 = new Error('rsc-error-log-nested', { cause: depth1 })" +
'\n 7 | console.error(depth0)' +
'\n 8 | } {' +
'\n [cause]: Error: Depth 2 error' +
'\n at logError (app/rsc-error-log-nested/page.js:4:18)' +
'\n at Page (app/rsc-error-log-nested/page.js:11:3)' +
"\n 2 | const depth4 = new Error('Depth 4 error')" +
"\n 3 | const depth3 = new Error('Depth 3 error', { cause: depth4 })" +
"\n > 4 | const depth2 = new Error('Depth 2 error', { cause: depth3 })" +
'\n | ^' +
"\n 5 | const depth1 = new Error('Depth 1 error', { cause: depth2 })" +
"\n 6 | const depth0 = new Error('rsc-error-log-nested', { cause: depth1 })" +
'\n 7 | console.error(depth0) {' +
'\n [cause]: [Error]' +
'\n }' +
'\n }' +
'\n}'
)
// Verify depth 3+ are NOT shown (truncated to [Error])
expect(
normalizeCliOutput(next.cliOutput.slice(outputIndex))
).not.toContain('Error: Depth 3 error')
expect(
normalizeCliOutput(next.cliOutput.slice(outputIndex))
).not.toContain('Error: Depth 4 error')
} else {
if (isTurbopack) {
// TODO(veil): Sourcemap names
// TODO(veil): relative paths in production
expect(normalizeCliOutput(next.cliOutput)).toContain(
'(app/rsc-error-log-nested/page.js:6:18)'
)
expect(normalizeCliOutput(next.cliOutput)).toContain('[cause]: [Error]')
expect(normalizeCliOutput(next.cliOutput)).not.toContain(
'Error: Depth 3 error'
)
expect(normalizeCliOutput(next.cliOutput)).not.toContain(
'Error: Depth 4 error'
)
} else {
// TODO(veil): line/column numbers are flaky in Webpack
}
}
})
it('logged errors include `[errors]` for AggregateError', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
await next.render('/rsc-error-log-aggregate')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'AggregateError: rsc-error-log-aggregate'
)
})
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'AggregateError: rsc-error-log-aggregate' +
'\n at logError (app/rsc-error-log-aggregate/page.js:6:26)' +
'\n at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
"\n 4 | const error2 = new TypeError('Error 2')" +
"\n 5 | const rootError = new Error('Root error')" +
'\n> 6 | const aggregateError = new AggregateError(' +
'\n | ^' +
'\n 7 | [error1, error2],' +
"\n 8 | 'rsc-error-log-aggregate'," +
'\n 9 | { cause: rootError } {' +
'\n [cause]: Error: Root error' +
'\n at logError (app/rsc-error-log-aggregate/page.js:5:21)' +
'\n at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
"\n 3 | const error1 = new Error('Error 1')" +
"\n 4 | const error2 = new TypeError('Error 2')" +
"\n > 5 | const rootError = new Error('Root error')" +
'\n | ^' +
'\n 6 | const aggregateError = new AggregateError(' +
'\n 7 | [error1, error2],' +
"\n 8 | 'rsc-error-log-aggregate',," +
'\n [errors]: [' +
'\n Error: Error 1' +
'\n at logError (app/rsc-error-log-aggregate/page.js:3:18)' +
'\n at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
'\n 1 | /* global AggregateError */' +
'\n 2 | function logError() {' +
"\n > 3 | const error1 = new Error('Error 1')" +
'\n | ^' +
"\n 4 | const error2 = new TypeError('Error 2')" +
"\n 5 | const rootError = new Error('Root error')" +
'\n 6 | const aggregateError = new AggregateError(,' +
'\n TypeError: Error 2' +
'\n at logError (app/rsc-error-log-aggregate/page.js:4:18)' +
'\n at Page (app/rsc-error-log-aggregate/page.js:15:3)' +
'\n 2 | function logError() {' +
"\n 3 | const error1 = new Error('Error 1')" +
"\n > 4 | const error2 = new TypeError('Error 2')" +
'\n | ^' +
"\n 5 | const rootError = new Error('Root error')" +
'\n 6 | const aggregateError = new AggregateError(' +
'\n 7 | [error1, error2],' +
'\n ]' +
'\n}'
)
} else {
if (isTurbopack) {
// TODO(veil): Sourcemap names
// TODO(veil): relative paths in production
expect(normalizeCliOutput(next.cliOutput)).toContain(
'(app/rsc-error-log-aggregate/page.js:6:26)'
)
expect(normalizeCliOutput(next.cliOutput)).toContain('[errors]:')
expect(normalizeCliOutput(next.cliOutput)).toContain('[cause]:')
} else {
// TODO(veil): line/column numbers are flaky in Webpack
}
}
})
it('prints the errors of an AggregateError behind a `cause`', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
await next.render('/rsc-error-log-aggregate-nested')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: rsc-error-log-aggregate-nested'
)
})
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'Error: rsc-error-log-aggregate-nested' +
'\n at logError (app/rsc-error-log-aggregate-nested/page.js:7:5)' +
'\n at Page (app/rsc-error-log-aggregate-nested/page.js:12:3)' +
"\n 5 | const aggregateError = new AggregateError([error1, error2], 'Aggregate')" +
'\n 6 | console.error(' +
"\n> 7 | new Error('rsc-error-log-aggregate-nested', { cause: aggregateError })" +
'\n | ^' +
'\n 8 | )' +
'\n 9 | }' +
'\n 10 | {' +
'\n [cause]: AggregateError: Aggregate' +
'\n at logError (app/rsc-error-log-aggregate-nested/page.js:5:26)' +
'\n at Page (app/rsc-error-log-aggregate-nested/page.js:12:3)' +
"\n 3 | const error1 = Object.assign(new Error('Error 1'), { code: 'ERR_ONE' })" +
"\n 4 | const error2 = Object.assign(new TypeError('Error 2'), { code: 'ERR_TWO' })" +
"\n > 5 | const aggregateError = new AggregateError([error1, error2], 'Aggregate')" +
'\n | ^' +
'\n 6 | console.error(' +
"\n 7 | new Error('rsc-error-log-aggregate-nested', { cause: aggregateError })" +
'\n 8 | ) {' +
'\n [errors]: [' +
'\n Error: Error 1' +
'\n at logError (app/rsc-error-log-aggregate-nested/page.js:3:32)' +
'\n at Page (app/rsc-error-log-aggregate-nested/page.js:12:3)' +
'\n 1 | /* global AggregateError */' +
'\n 2 | function logError() {' +
"\n > 3 | const error1 = Object.assign(new Error('Error 1'), { code: 'ERR_ONE' })" +
'\n | ^' +
"\n 4 | const error2 = Object.assign(new TypeError('Error 2'), { code: 'ERR_TWO' })" +
"\n 5 | const aggregateError = new AggregateError([error1, error2], 'Aggregate')" +
'\n 6 | console.error( {' +
"\n code: 'ERR_ONE'" +
'\n },' +
'\n TypeError: Error 2' +
'\n at logError (app/rsc-error-log-aggregate-nested/page.js:4:32)' +
'\n at Page (app/rsc-error-log-aggregate-nested/page.js:12:3)' +
'\n 2 | function logError() {' +
"\n 3 | const error1 = Object.assign(new Error('Error 1'), { code: 'ERR_ONE' })" +
"\n > 4 | const error2 = Object.assign(new TypeError('Error 2'), { code: 'ERR_TWO' })" +
'\n | ^' +
"\n 5 | const aggregateError = new AggregateError([error1, error2], 'Aggregate')" +
'\n 6 | console.error(' +
"\n 7 | new Error('rsc-error-log-aggregate-nested', { cause: aggregateError }) {" +
"\n code: 'ERR_TWO'" +
'\n }' +
'\n ]' +
'\n }' +
'\n}'
)
// The errors of the AggregateError carry own properties, so Node.js
// applies its depth limit to them. They must not collapse to `[Error]`.
expect(
normalizeCliOutput(next.cliOutput.slice(outputIndex))
).not.toContain('[Error]')
} else {
if (isTurbopack) {
// TODO(veil): Sourcemap names
//
// TODO(veil): relative paths in production
expect(normalizeCliOutput(next.cliOutput)).toContain(
'(app/rsc-error-log-aggregate-nested/page.js:7:5)'
)
expect(normalizeCliOutput(next.cliOutput)).toContain('[cause]:')
expect(normalizeCliOutput(next.cliOutput)).toContain('[errors]:')
expect(normalizeCliOutput(next.cliOutput)).toContain("code: 'ERR_ONE'")
expect(normalizeCliOutput(next.cliOutput)).toContain("code: 'ERR_TWO'")
} else {
// TODO(veil): line/column numbers are flaky in Webpack
}
}
})
it('logged errors in client components during ssr have a sourcemapped stack with a codeframe', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
await next.render('/ssr-error-log')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: ssr-error-log'
)
})
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'Error: ssr-error-log' +
'\n at logError (app/ssr-error-log/page.js:4:17)' +
'\n at Page (app/ssr-error-log/page.js:9:3)' +
'\n 2 |' +
'\n 3 | function logError() {' +
"\n> 4 | const error = new Error('ssr-error-log')" +
'\n | ^' +
'\n 5 | console.error(error)' +
'\n 6 | }' +
'\n 7 |' +
'\n'
)
} else {
if (isTurbopack) {
// TODO(veil): Sourcemap names
expect(normalizeCliOutput(next.cliOutput)).toContain(
'Error: ssr-error-log' +
'\n at <unknown> (app/ssr-error-log/page.js:4:17)' +
'\n 2 |' +
'\n 3 | function logError() {' +
"\n> 4 | const error = new Error('ssr-error-log')" +
'\n | ^' +
'\n 5 | console.error(error)' +
'\n 6 | }' +
'\n 7 |' +
'\n'
)
} else {
// TODO(veil): line/column numbers are flaky in Webpack
}
}
})
it('stack frames are ignore-listed in ssr', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/ssr-error-log-ignore-listed')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: ssr-error-log-ignore-listed'
)
})
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
isTurbopack
? 'Error: ssr-error-log-ignore-listed' +
'\n at logError (app/ssr-error-log-ignore-listed/page.js:9:17)' +
'\n at runWithInternalIgnored (app/ssr-error-log-ignore-listed/page.js:19:13)' +
'\n at runWithExternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:18:29)' +
'\n at runWithExternal (app/ssr-error-log-ignore-listed/page.js:17:32)' +
'\n at runWithInternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:16:18)' +
// Realpath does not point into node_modules so we don't ignore it.
'\n at runInternalSourceMapped (internal-pkg/sourcemapped.ts:5:10)' +
'\n at runWithInternal (app/ssr-error-log-ignore-listed/page.js:15:28)' +
// Realpath does not point into node_modules so we don't ignore it.
'\n at runInternal (internal-pkg/index.js:2:10)' +
'\n at Page (app/ssr-error-log-ignore-listed/page.js:14:14)' +
'\n 7 |' +
'\n'
: 'Error: ssr-error-log-ignore-listed' +
'\n at logError (app/ssr-error-log-ignore-listed/page.js:9:17)' +
'\n at runWithInternalIgnored (app/ssr-error-log-ignore-listed/page.js:19:13)' +
// TODO(veil-NDX-910): Webpacks's sourcemap loader drops `ignoreList`
// TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
// Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
'\n at runInternalIgnored (webpack-internal:/(ssr)/internal-pkg/ignored.ts:6:10)' +
'\n at runWithExternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:18:29)' +
'\n at runWithExternal (app/ssr-error-log-ignore-listed/page.js:17:32)' +
'\n at runWithInternalSourceMapped (app/ssr-error-log-ignore-listed/page.js:16:18)' +
// TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
// Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
// Realpath does not point into node_modules so we don't ignore it.
'\n at runInternalSourceMapped (webpack-internal:/(ssr)/internal-pkg/sourcemapped.ts:5:10)' +
'\n at runWithInternal (app/ssr-error-log-ignore-listed/page.js:15:28)' +
// Realpath does not point into node_modules so we don't ignore it.
'\n at runInternal (internal-pkg/index.js:2:10)' +
'\n at Page (app/ssr-error-log-ignore-listed/page.js:14:14)' +
'\n 7 |' +
'\n'
)
if (isTurbopack) {
// TODO(veil): Turbopack errors because it thinks the sources are not part of the project.
await expect(browser).toDisplayCollapsedRedbox(`
{
"description": "ssr-error-log-ignore-listed",
"environmentLabel": null,
"label": "Console Error",
"source": "app/ssr-error-log-ignore-listed/page.js (9:17) @ logError
> 9 | const error = new Error('ssr-error-log-ignore-listed')
| ^",
"stack": [
"logError app/ssr-error-log-ignore-listed/page.js (9:17)",
"runWithInternalIgnored app/ssr-error-log-ignore-listed/page.js (19:13)",
"runWithExternalSourceMapped app/ssr-error-log-ignore-listed/page.js (18:29)",
"runWithExternal app/ssr-error-log-ignore-listed/page.js (17:32)",
"runWithInternalSourceMapped app/ssr-error-log-ignore-listed/page.js (16:18)",
"runInternalSourceMapped internal-pkg/sourcemapped.ts (5:10)",
"runWithInternal app/ssr-error-log-ignore-listed/page.js (15:28)",
"runInternal internal-pkg/index.js (2:10)",
"Page app/ssr-error-log-ignore-listed/page.js (14:14)",
],
}
`)
} else {
// TODO(veil-NDX-910): Webpacks's sourcemap loader drops `ignoreList`
// TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
await expect(browser).toDisplayCollapsedRedbox(`
{
"description": "ssr-error-log-ignore-listed",
"environmentLabel": null,
"label": "Console Error",
"source": "app/ssr-error-log-ignore-listed/page.js (9:17) @ logError
> 9 | const error = new Error('ssr-error-log-ignore-listed')
| ^",
"stack": [
"logError app/ssr-error-log-ignore-listed/page.js (9:17)",
"runWithInternalIgnored app/ssr-error-log-ignore-listed/page.js (19:13)",
"runInternalIgnored ignored.ts (6:10)",
"runWithExternalSourceMapped app/ssr-error-log-ignore-listed/page.js (18:29)",
"runWithExternal app/ssr-error-log-ignore-listed/page.js (17:32)",
"runWithInternalSourceMapped app/ssr-error-log-ignore-listed/page.js (16:18)",
"runInternalSourceMapped sourcemapped.ts (5:10)",
"runWithInternal app/ssr-error-log-ignore-listed/page.js (15:28)",
"runInternal internal-pkg/index.js (2:10)",
"Page app/ssr-error-log-ignore-listed/page.js (14:14)",
],
}
`)
}
} else {
if (isTurbopack) {
// TODO(veil): Sourcemap names
// TODO(veil): relative paths
expect(normalizeCliOutput(next.cliOutput)).toContain(
'(app/ssr-error-log-ignore-listed/page.js:9:17)'
)
expect(normalizeCliOutput(next.cliOutput)).toContain(
'\n' +
"> 9 | const error = new Error('ssr-error-log-ignore-listed')\n" +
' | ^\n'
)
} else {
// TODO(veil): line/column numbers are flaky in Webpack
}
}
})
it('stack frames are ignore-listed in rsc', async () => {
const outputIndex = next.cliOutput.length
await next.render('/rsc-error-log-ignore-listed')
if (isNextDev) {
await retry(() => {
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'Error: rsc-error-log-ignore-listed'
)
})
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
isTurbopack
? 'Error: rsc-error-log-ignore-listed' +
'\n at logError (app/rsc-error-log-ignore-listed/page.js:8:17)' +
'\n at runWithInternalIgnored (app/rsc-error-log-ignore-listed/page.js:18:13)' +
'\n at runWithExternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:17:29)' +
'\n at runWithExternal (app/rsc-error-log-ignore-listed/page.js:16:32)' +
'\n at runWithInternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:15:18)' +
// Realpath does not point into node_modules so we don't ignore it.
'\n at runInternalSourceMapped (internal-pkg/sourcemapped.ts:5:10)' +
'\n at runWithInternal (app/rsc-error-log-ignore-listed/page.js:14:28)' +
// Realpath does not point into node_modules so we don't ignore it.
'\n at runInternal (internal-pkg/index.js:2:10)' +
'\n at Page (app/rsc-error-log-ignore-listed/page.js:13:14)' +
'\n 6 |' +
'\n'
: 'Error: rsc-error-log-ignore-listed' +
'\n at logError (app/rsc-error-log-ignore-listed/page.js:8:17)' +
'\n at runWithInternalIgnored (app/rsc-error-log-ignore-listed/page.js:18:13)' +
// TODO(veil): Webpacks's sourcemap loader drops `ignoreList`
// TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
// Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
'\n at runInternalIgnored (webpack-internal:/(rsc)/internal-pkg/ignored.ts:6:10)' +
'\n at runWithExternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:17:29)' +
'\n at runWithExternal (app/rsc-error-log-ignore-listed/page.js:16:32)' +
'\n at runWithInternalSourceMapped (app/rsc-error-log-ignore-listed/page.js:15:18)' +
// TODO(veil): Webpack's sourcemap loader creates an incorrect `sources` entry.
// Can be worked around by using `./sourcemapped.ts` instead of `sourcemapped.ts`.
// Realpath does not point into node_modules so we don't ignore it.
'\n at runInternalSourceMapped (webpack-internal:/(rsc)/internal-pkg/sourcemapped.ts:5:10)' +
'\n at runWithInternal (app/rsc-error-log-ignore-listed/page.js:14:28)' +
// Realpath does not point into node_modules so we don't ignore it.
'\n at runInternal (internal-pkg/index.js:2:10)' +
'\n at Page (app/rsc-error-log-ignore-listed/page.js:13:14)' +
'\n 6 |' +
'\n'
)
} else {
if (isTurbopack) {
// TODO(veil): Sourcemap names
// TODO(veil): relative paths
expect(normalizeCliOutput(next.cliOutput)).toContain(
'at <unknown> (app/rsc-error-log-ignore-listed/page.js:8:17)'
)
expect(normalizeCliOutput(next.cliOutput)).toContain(
'' +
"\n> 8 | const error = new Error('rsc-error-log-ignore-listed')" +
'\n | ^'
)
} else {
// TODO(veil): line/column numbers are flaky in Webpack
}
}
})
it('thrown SSR errors', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/ssr-throw')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain('Error: ssr-throw')
})
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
expect(cliOutput).toContain(
' Error: ssr-throw' +
'\n at throwError (app/ssr-throw/Thrower.js:4:9)' +
'\n at Thrower (app/ssr-throw/Thrower.js:8:3)' +
'\n 2 |' +
'\n 3 | function throwError() {' +
"\n> 4 | throw new Error('ssr-throw')" +
'\n | ^' +
'\n 5 | }' +
'\n 6 |' +
'\n 7 | export function Thrower() { {' +
"\n digest: '"
)
expect(cliOutput).toMatch(/digest: '\d+'/)
await expect(browser).toDisplayRedbox(`
{
"description": "ssr-throw",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/ssr-throw/Thrower.js (4:9) @ throwError
> 4 | throw new Error('ssr-throw')
| ^",
"stack": [
"throwError app/ssr-throw/Thrower.js (4:9)",
"Thrower app/ssr-throw/Thrower.js (8:3)",
"Page app/ssr-throw/page.js (6:10)",
],
}
`)
} else {
// SSR errors are not logged because React retries them during hydration.
}
})
it('thrown errors from "use cache" have a sourcemapped stack with a codeframe', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
await next.render('/rsc-error-throw-cached')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: rsc-error-throw-cached'
)
})
const cliOutput = normalizeCliOutput(next.cliOutput.slice(outputIndex))
expect(cliOutput).toContain(
'Error: rsc-error-throw-cached' +
'\n at throwsInCache (app/rsc-error-throw-cached/page.js:5:9)' +
'\n 3 | async function throwsInCache() {' +
"\n 4 | 'use cache'" +
"\n> 5 | throw new Error('rsc-error-throw-cached')" +
'\n | ^' +
'\n 6 | }' +
'\n 7 |' +
'\n 8 | export default async function Page() {'
)
// The logged error is the one revived by the consuming Flight client,
// not the original from the cache environment.
expect(cliOutput).toContain("environmentName: 'Cache'")
} else {
const outputIndex = next.cliOutput.length
await next.render('/rsc-error-throw-cached')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: rsc-error-throw-cached'
)
})
// React only creates fake stack frames in development.
expect(
normalizeCliOutput(next.cliOutput.slice(outputIndex))
).not.toContain('about://React/')
}
})
it('logged errors preserve their name', async () => {
let cliOutput = next.cliOutput
if (isNextDev) {
const outputIndex = next.cliOutput.length
await next.render('/rsc-error-log-custom-name')
cliOutput = next.cliOutput.slice(outputIndex)
}
await retry(() => {
expect(cliOutput).toContain(
// TODO: isNextDev ? 'UnnamedError: rsc-error-log-custom-name-Foo' : '[Error]: rsc-error-log-custom-name-Foo'
isNextDev
? 'Error: rsc-error-log-custom-name-Foo'
: 'Error: rsc-error-log-custom-name-Foo'
)
})
expect(cliOutput).toContain(
// TODO: isNextDev ? 'NamedError [MyError]: rsc-error-log-custom-name-Bar' : '[MyError]: rsc-error-log-custom-name-Bar'
isNextDev
? 'Error [MyError]: rsc-error-log-custom-name-Bar'
: 'Error [MyError]: rsc-error-log-custom-name-Bar'
)
})
it('does not print a stack frame for the error message line', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
await next.render('/rsc-error-log-location-message')
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: rsc-error-log-location-message'
)
})
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'Error: rsc-error-log-location-message: connect ECONNREFUSED ::1:45999' +
'\n at logError (app/rsc-error-log-location-message/page.js:7:5)' +
'\n at Page (app/rsc-error-log-location-message/page.js:12:3)' +
'\n 5 | // test match this error only.' +
'\n 6 | console.error(' +
"\n> 7 | new Error('rsc-error-log-location-message: connect ECONNREFUSED ::1:45999')" +
'\n | ^' +
'\n 8 | )' +
'\n 9 | }'
)
// `stacktrace-parser` reads the `<name>: <message>` line as a frame when
// the message ends with text that looks like a file location. A real
// anonymous frame also reads `at <unknown>`, so this assertion names the
// message to keep the two apart.
expect(
normalizeCliOutput(next.cliOutput.slice(outputIndex))
).not.toContain('at <unknown> (Error: rsc-error-log-location-message')
} else {
if (isTurbopack) {
// TODO(veil): Sourcemap names
//
// TODO(veil): relative paths in production
expect(normalizeCliOutput(next.cliOutput)).toContain(
'(app/rsc-error-log-location-message/page.js:7:5)'
)
expect(normalizeCliOutput(next.cliOutput)).not.toContain(
'at <unknown> (Error: rsc-error-log-location-message'
)
} else {
// TODO(veil): line/column numbers are flaky in Webpack
}
}
})
it('handles invalid sourcemaps gracefully', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
await next.render('/bad-sourcemap')
await retry(() => {
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'Error: bad-sourcemap'
)
})
if (isTurbopack) {
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
// Node.js is fine with invalid URLs in index maps apparently.
'' +
'Error: bad-sourcemap' +
'\n at logError (app/bad-sourcemap/custom:/[badhost]/app/bad-sourcemap/page.js:6:17)' +
'\n at Page (app/bad-sourcemap/custom:/[badhost]/app/bad-sourcemap/page.js:10:3)' +
'\n'
)
} else {
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
// Node.js is not fine with invalid URLs in vanilla source maps.
// Feel free to adjust these locations. They're just here to showcase
// sourcemapping is broken on invalid sources.
'' +
`\nwebpack-internal:///(rsc)/./app/bad-sourcemap/page.js: Invalid source map. Only conformant source maps can be used to find the original code. Cause: TypeError [ERR_INVALID_ARG_TYPE]: The "payload" argument must be of type object. Received null` +
'\nError: bad-sourcemap' +
'\n at logError (webpack-internal:///(rsc)/./app/bad-sourcemap/page.js:12:19)' +
'\n at Page (webpack-internal:///(rsc)/./app/bad-sourcemap/page.js:15:5)'
)
// Expect the invalid sourcemap warning only once per render.
// Dynamic I/O renders three times.
// One from filterStackFrameDEV.
// One from findSourceMapURLDEV.
expect(
normalizeCliOutput(next.cliOutput.slice(outputIndex)).split(
'Invalid source map.'
).length - 1
).toEqual(3)
}
} else {
// Bundlers silently drop invalid sourcemaps.
expect(
normalizeCliOutput(next.cliOutput).split('Invalid source map.').length -
1
).toEqual(0)
}
})
it('sourcemaps errors during module evaluation', async () => {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/module-evaluation')
if (isNextDev) {
await retry(() => {
expect(next.cliOutput.slice(outputIndex)).toContain(
'Error: module-evaluation'
)
})
const cliOutput = stripAnsi(next.cliOutput.slice(outputIndex))
if (isTurbopack) {
expect(cliOutput).toContain(
'Error: module-evaluation' +
// TODO(veil): Should map to no name like you'd get with native stacks without a bundler.
'\n at module evaluation (app/module-evaluation/module.js:1:22)' +
// TODO(veil): Added frames from bundler should be sourcemapped (https://linear.app/vercel/issue/NDX-509/)
'\n at module evaluation (app/module-evaluation/page.js:1:1)' +
'\n at module evaluation (.next'
)
} else {
expect(cliOutput).toContain(
'Error: module-evaluation' +
// TODO(veil): Should map to no name like you'd get with native stacks without a bundler.
// TODO(veil): Location should be sourcemapped
'\n at eval (app/module-evaluation/module.js:1:22)' +
// TODO(veil): Added frames from bundler should be sourcemapped (https://linear.app/vercel/issue/NDX-509/)
'\n at <unknown> (rsc)/.'
)
}
expect(cliOutput).toContain(
'' +
"\n> 1 | export const error = new Error('module-evaluation')" +
'\n | ^'
)
if (isTurbopack) {
await expect(browser).toDisplayCollapsedRedbox(`
{
"description": "module-evaluation",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/module-evaluation/module.js (1:22) @ module evaluation
> 1 | export const error = new Error('module-evaluation')
| ^",
"stack": [
"module evaluation app/module-evaluation/module.js (1:22)",
"module evaluation app/module-evaluation/page.js (1:1)",
"module evaluation app/module-evaluation/page.js (6:1)",
"Page <anonymous>",
],
}
`)
} else if (isRspack) {
await expect(browser).toDisplayCollapsedRedbox(`
{
"description": "module-evaluation",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/module-evaluation/module.js (1:22) @ eval
> 1 | export const error = new Error('module-evaluation')
| ^",
"stack": [
"eval app/module-evaluation/module.js (1:22)",
"<FIXME-file-protocol>",
"<FIXME-file-protocol>",
"eval about:/Prerender/webpack-internal:///(rsc)/app/module-evaluation/page.js (5:45)",
"<FIXME-file-protocol>",
"<FIXME-file-protocol>",
"Page <anonymous>",
],
}
`)
} else {
await expect(browser).toDisplayCollapsedRedbox(`
{
"description": "module-evaluation",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/module-evaluation/module.js (1:22) @ eval
> 1 | export const error = new Error('module-evaluation')
| ^",
"stack": [
"eval app/module-evaluation/module.js (1:22)",
"<FIXME-file-protocol>",
"eval about:/Prerender/webpack-internal:///(rsc)/app/module-evaluation/page.js (5:65)",
"<FIXME-file-protocol>",
"Page <anonymous>",
],
}
`)
}
} else {
if (isTurbopack) {
expect(normalizeCliOutput(next.cliOutput)).toContain(
'' +
'\nError: module-evaluation' +
// TODO(veil): Turbopack internals. Feel free to update. Tracked in https://linear.app/vercel/issue/NEXT-4362
'\n at module evaluation (app/module-evaluation/module.js:1:22)'
)
expect(normalizeCliOutput(next.cliOutput)).toContain(
'' +
"\n> 1 | export const error = new Error('module-evaluation')" +
'\n | ^'
)
} else {
expect(
normalizeCliOutput(next.cliOutput).replaceAll(
/at \d+ /g,
'at <WebpackModuleID> '
)
).toContain(
'' +
'\nError: module-evaluation' +
// TODO(veil): column numbers are flaky in Webpack
'\n at <WebpackModuleID> (bundler:///app/module-evaluation/module.js:1:'
)
}
}
})
it('ignore-lists anonymous rsc stack frame sandwiches', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/rsc-anonymous-stack-frame-sandwich')
// TODO(veil): Implement sandwich heuristic in `filterStackFrameDEV`
if (isTurbopack) {
await expect(browser).toDisplayCollapsedRedbox(`
[
{
"description": "rsc-anonymous-stack-frame-sandwich: external",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (5:29) @ Page
> 5 | runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')
| ^",
"stack": [
"Set.forEach <anonymous>",
"Set.forEach <anonymous>",
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (5:29)",
"Page <anonymous>",
],
},
{
"description": "rsc-anonymous-stack-frame-sandwich: internal",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
> 6 | runHiddenSetOfSetsInternal('rsc-anonymous-stack-frame-sandwich: internal')
| ^",
"stack": [
"Set.forEach <anonymous>",
"Set.forEach <anonymous>",
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (6:29)",
"Page <anonymous>",
],
},
]
`)
} else if (isRspack) {
// 2nd error from runHiddenSetOfSetsInternal hits https://linear.app/vercel/issue/NEXT-4412
await expect(browser).toDisplayCollapsedRedbox(`
[
{
"description": "rsc-anonymous-stack-frame-sandwich: external",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (5:29) @ Page
> 5 | runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')
| ^",
"stack": [
"Set.forEach <anonymous>",
"Set.forEach <anonymous>",
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (5:29)",
"Page <anonymous>",
],
},
{
"description": "rsc-anonymous-stack-frame-sandwich: internal",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
> 6 | runHiddenSetOfSetsInternal('rsc-anonymous-stack-frame-sandwich: internal')
| ^",
"stack": [
"eval webpack-internal:/(ssr)/internal-pkg/ignored.ts (18:54)",
"eval webpack-internal:/(ssr)/internal-pkg/ignored.ts (12:7)",
"Set.forEach <anonymous>",
"eval webpack-internal:/(ssr)/internal-pkg/ignored.ts (11:9)",
"Set.forEach <anonymous>",
"runSetOfSets webpack-internal:/(ssr)/internal-pkg/ignored.ts (10:13)",
"runHiddenSetOfSets webpack-internal:/(ssr)/internal-pkg/ignored.ts (18:3)",
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (6:29)",
"Page <anonymous>",
],
},
]
`)
} else {
// 2nd error from runHiddenSetOfSetsInternal hits https://linear.app/vercel/issue/NEXT-4412
await expect(browser).toDisplayCollapsedRedbox(`
[
{
"description": "rsc-anonymous-stack-frame-sandwich: external",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (5:29) @ Page
> 5 | runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')
| ^",
"stack": [
"Set.forEach <anonymous>",
"Set.forEach <anonymous>",
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (5:29)",
"Page <anonymous>",
],
},
{
"description": "rsc-anonymous-stack-frame-sandwich: internal",
"environmentLabel": "Prerender",
"label": "Console Error",
"source": "app/rsc-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
> 6 | runHiddenSetOfSetsInternal('rsc-anonymous-stack-frame-sandwich: internal')
| ^",
"stack": [
"eval webpack-internal:/(rsc)/internal-pkg/ignored.ts (18:54)",
"eval webpack-internal:/(rsc)/internal-pkg/ignored.ts (12:7)",
"Set.forEach <anonymous>",
"eval webpack-internal:/(rsc)/internal-pkg/ignored.ts (11:9)",
"Set.forEach <anonymous>",
"runSetOfSets webpack-internal:/(rsc)/internal-pkg/ignored.ts (10:13)",
"runHiddenSetOfSets webpack-internal:/(rsc)/internal-pkg/ignored.ts (18:3)",
"Page app/rsc-anonymous-stack-frame-sandwich/page.js (6:29)",
"Page <anonymous>",
],
},
]
`)
}
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'' +
'Error: rsc-anonymous-stack-frame-sandwich: external' +
'\n at Page (app/rsc-anonymous-stack-frame-sandwich/page.js:5:29)' +
'\n 3 |' +
'\n 4 | export default function Page() {' +
"\n> 5 | runHiddenSetOfSetsExternal('rsc-anonymous-stack-frame-sandwich: external')" +
'\n | ^'
)
// TODO: assert on 2nd error once that's bug free
} else {
// TODO(veil): assert on 1st error once cursor position is consistent
// TODO(veil): assert on 2nd error once that's bug free
}
})
it('ignore-lists anonymous ssr stack frame sandwiches', async () => {
if (isNextDev) {
const outputIndex = next.cliOutput.length
const browser = await next.browser('/ssr-anonymous-stack-frame-sandwich')
if (isTurbopack) {
await expect(browser).toDisplayCollapsedRedbox(`
[
{
"description": "ssr-anonymous-stack-frame-sandwich: external",
"environmentLabel": null,
"label": "Console Error",
"source": "app/ssr-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
> 6 | runHiddenSetOfSetsExternal('ssr-anonymous-stack-frame-sandwich: external')
| ^",
"stack": [
"Page app/ssr-anonymous-stack-frame-sandwich/page.js (6:29)",
],
},
{
"description": "ignore-listed frames",
"environmentLabel": null,
"label": "Console Error",
"source": "app/ssr-anonymous-stack-frame-sandwich/page.js (7:29) @ Page
> 7 | runHiddenSetOfSetsInternal('ssr-anonymous-stack-frame-sandwich: internal')
| ^",
"stack": [
"<unknown> internal-pkg/sourcemapped.ts (18:43)",
"<unknown> internal-pkg/sourcemapped.ts (11:7)",
"Set.forEach <anonymous>",
"<unknown> internal-pkg/sourcemapped.ts (10:9)",
"Set.forEach <anonymous>",
"runSetOfSets internal-pkg/sourcemapped.ts (9:13)",
"runHiddenSetOfSets internal-pkg/sourcemapped.ts (17:3)",
"Page app/ssr-anonymous-stack-frame-sandwich/page.js (7:29)",
],
},
]
`)
} else {
// 2nd error from runHiddenSetOfSetsInternal hits https://linear.app/vercel/issue/NEXT-4412
await expect(browser).toDisplayCollapsedRedbox(`
[
{
"description": "ssr-anonymous-stack-frame-sandwich: external",
"environmentLabel": null,
"label": "Console Error",
"source": "app/ssr-anonymous-stack-frame-sandwich/page.js (6:29) @ Page
> 6 | runHiddenSetOfSetsExternal('ssr-anonymous-stack-frame-sandwich: external')
| ^",
"stack": [
"Page app/ssr-anonymous-stack-frame-sandwich/page.js (6:29)",
],
},
{
"description": "ignore-listed frames",
"environmentLabel": null,
"label": "Console Error",
"source": "app/ssr-anonymous-stack-frame-sandwich/page.js (7:29) @ Page
> 7 | runHiddenSetOfSetsInternal('ssr-anonymous-stack-frame-sandwich: internal')
| ^",
"stack": [
"eval sourcemapped.ts (18:43)",
"eval sourcemapped.ts (11:7)",
"Set.forEach <anonymous>",
"eval sourcemapped.ts (10:9)",
"Set.forEach <anonymous>",
"runSetOfSets sourcemapped.ts (9:13)",
"runHiddenSetOfSets sourcemapped.ts (17:3)",
"Page app/ssr-anonymous-stack-frame-sandwich/page.js (7:29)",
],
},
]
`)
}
expect(normalizeCliOutput(next.cliOutput.slice(outputIndex))).toContain(
'' +
'Error: ssr-anonymous-stack-frame-sandwich: external' +
'\n at Page (app/ssr-anonymous-stack-frame-sandwich/page.js:6:29)' +
'\n 4 |' +
'\n 5 | export default function Page() {' +
"\n> 6 | runHiddenSetOfSetsExternal('ssr-anonymous-stack-frame-sandwich: external')" +
'\n | ^'
)
// TODO(veil): assert on 2nd error once that's bug free
} else {
// TODO(veil): assert on 1st error once cursor position is consistent
// TODO(veil): assert on 2nd error once that's bug free
}
})
})