vercel/next.js · #99888

Don't parse error message lines as stack frames in the server error inspector

lukesandberg · merged Oct 10, 20263 files · 52 + / 3 −
packages/next/src/server/patch-error-inspect.ts49 + / 2 −
@@ -164,16 +164,63 @@ function computeErrorName(error: Error): string {   return error.name || 'Error' } +// Records where the frames start in a stack produced by+// `prepareUnsourcemappedStackTrace`, so they can be parsed without having to+// guess where the message ends. Uses `Symbol.for` since this module is bundled+// into several runtimes, and any copy may have formatted a given stack.+const STACK_FRAMES_START = Symbol.for('next.dev.stackFramesStart')+type ErrorWithStackFramesStart = Error & {+  [STACK_FRAMES_START]?: { stack: string; framesStart: number }+}+ function prepareUnsourcemappedStackTrace(   error: Error,   structuredStackTrace: any[] ): string {   const name = computeErrorName(error)   const message = error.message || ''   let stack = name + ': ' + message+  const framesStart = stack.length   for (let i = 0; i < structuredStackTrace.length; i++) {     stack += '\n    at ' + structuredStackTrace[i].toString()   }+  // Record where the real stack frames start+  try {+    // Non-enumerable so that it doesn't show up when inspecting the error.+    Object.defineProperty(error, STACK_FRAMES_START, {+      value: { stack, framesStart },+      configurable: true,+      writable: true,+    })+  } catch {+    // The error may be frozen. We'll fall back to finding the frames by+    // matching the message.+  }+  return stack+}++/**+ * Returns the part of `error.stack` after the message, so the stack parser+ * can't mistake message lines for frames (e.g. a line starting with a URL or+ * with "webpack"/"turbopack").+ */+function getUnparsedStackFrames(error: Error, errorName: string): string {+  let stack = String(error.stack)+  const recorded = (error as ErrorWithStackFramesStart)[STACK_FRAMES_START]+  // `error.stack` may have been replaced after we formatted it.+  if (recorded !== undefined && recorded.stack === stack) {+    return stack.slice(recorded.framesStart)+  }+  // Errors printing by `prepareUnsourcemappedStackTrace` are `Error: <message>` so attempt to strip that prefix+  if (stack.startsWith(errorName)) {+    stack = stack.slice(errorName.length)+  }+  if (stack.startsWith(': ')) {+    stack = stack.slice(': '.length)+  }+  if (stack.startsWith(error.message)) {+    stack = stack.slice(error.message.length)+  }   return stack } @@ -435,12 +482,12 @@ function parseAndSourceMap(   inspectOptions: util.InspectOptions ): string {   const showIgnoreListed = process.env.__NEXT_SHOW_IGNORE_LISTED === 'true'-  // We overwrote Error.prepareStackTrace earlier so error.stack is not sourcemapped.-  let unparsedStack = String(error.stack)   // We could just read it from `error.stack`.   // This works around cases where a 3rd party `Error.prepareStackTrace` implementation   // doesn't implement the name computation correctly.   const errorName = computeErrorName(error)+  // We overwrote Error.prepareStackTrace earlier so error.stack is not sourcemapped.+  let unparsedStack = getUnparsedStackFrames(error, errorName)    let idx = unparsedStack.indexOf('react_stack_bottom_frame')   if (idx !== -1) {
test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts1 + / 1 −
@@ -37,7 +37,7 @@ describe('proxy-runtime', () => {        4 |        The exported configuration object in a source file needs to have a very specific format from which some properties can be statically parsed at compiled-time.        https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config-       at <unknown> (https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config)"+       at ignore-listed frames"       `)     } else {       expect(cliOutput).toContain(
test/e2e/turbopack-css-node-process-connect-failure/index.test.ts2 + / 0 −
@@ -59,5 +59,7 @@ describe('turbopack css node process connect failure', () => {     // This is not a Turbopack bug, so don't present it as one.     expect(cliOutput).not.toContain('TurbopackInternalError')     expect(cliOutput).not.toContain('https://bugs.nextjs.org/')+    // Lines of the message must not be mistaken for stack frames.+    expect(cliOutput).not.toContain('at <unknown>')   }) })