Files
b51ca60fd4 Fix dev overlay symbolication for project paths needing percent-encoding (#96221)
In a project whose absolute path contains characters that percent-encode
in URLs — e.g. a space — the dev overlay showed no code frame and raw
`file://` URLs instead of source locations for server frames.

Two encoding mismatches caused this:

- Turbopack's `trace_source` percent-decoded the source map's original
file URL before comparing it against the still-encoded project root URI,
so the containment check failed ("Original file ... outside project")
for any project path with such characters. The comparisons now stay in
the encoded domain and only the outputs are decoded back into paths.

- React synthesizes stack frame `file:` URLs by prepending the scheme to
a filesystem path, so the overlay receives URLs with e.g. raw spaces.
These aren't well-formed, and they don't match the `url.pathToFileURL`
form Node.js keys its source map cache by. The overlay middleware now
re-encodes them through WHATWG URL parsing, which tolerates such input.

## Test Plan

New tests.

Known cases that remain broken are commented out with a TODO.
These could be resolved if https://github.com/react/react/pull/37105 or
equivalent lands.

## Before

<img width="825" height="732" alt="Screenshot 2026-07-25 at 14 39 18"
src="https://github.com/user-attachments/assets/0f119e52-ec86-495a-bf5c-5339faa1d606"
/>


## After

<img width="1126" height="733" alt="Screenshot 2026-07-25 at 14 58 14"
src="https://github.com/user-attachments/assets/a263ee8a-4cfe-4358-9a8e-2e58b4b16aa5"
/>

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-25 18:36:45 +01:00

91 lines
2.6 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 { getRedboxSource, retry } from 'next-test-utils'
function setup(subDir: string) {
return nextTestSetup({
files: path.join(__dirname, 'fixtures'),
subDir,
})
}
async function assertSymbolicatedSSRError(
next: ReturnType<typeof setup>['next']
) {
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)'
)
let redboxSource: string | null = null
await retry(async () => {
redboxSource = await getRedboxSource(browser)
expect(redboxSource).not.toBeNull()
})
expect(redboxSource).toContain('app/ssr-throw/Thrower.js (4:9) @ throwError')
expect(redboxSource).toContain("throw new Error('ssr-throw')")
}
// Symbolication must work in project directories whose absolute path
// contains characters that need percent-encoding in URLs.
describe('special project paths', () => {
describe('in "space dir"', () => {
const { skipped, next } = setup('space dir')
if (skipped) return
it('symbolicates thrown SSR errors', async () => {
await assertSymbolicatedSSRError(next)
})
})
describe('in "ünïcode-dir"', () => {
const { skipped, next } = setup('ünïcode-dir')
if (skipped) return
it('symbolicates thrown SSR errors', async () => {
await assertSymbolicatedSSRError(next)
})
})
describe('in "bracket [dir]"', () => {
const { skipped, next } = setup('bracket [dir]')
if (skipped) return
it('symbolicates thrown SSR errors', async () => {
await assertSymbolicatedSSRError(next)
})
})
// TODO(veil): The remaining directory names need React's fake stack frame
// filename virtualization to round-trip losslessly. When they start
// passing, `it.failing` will fail and remind us to promote them.
describe('in "percent%20dir"', () => {
const { skipped, next } = setup('percent%20dir')
if (skipped) return
it.failing('symbolicates thrown SSR errors', async () => {
await assertSymbolicatedSSRError(next)
})
})
describe('in "hash#dir"', () => {
const { skipped, next } = setup('hash#dir')
if (skipped) return
it.failing('symbolicates thrown SSR errors', async () => {
await assertSymbolicatedSSRError(next)
})
})
})