mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
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]>
91 lines
2.6 KiB
TypeScript
91 lines
2.6 KiB
TypeScript
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)
|
||
})
|
||
})
|
||
})
|