Skip to content

Commit 3172cfe

Browse files
omarmciverOmar McIverhuozhi
authored
fix: support both decoded and encoded url requests of conventioned files (#56187)
Co-authored-by: Omar McIver <[email protected]> Co-authored-by: Jiachi Liu <[email protected]>
1 parent a2f9ef5 commit 3172cfe

File tree

4 files changed

+106
-83
lines changed

4 files changed

+106
-83
lines changed

packages/next/src/server/lib/router-utils/filesystem.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -517,11 +517,25 @@ export async function setupFsCheck(opts: {
517517
} catch {}
518518
}
519519

520+
let matchedItem = items.has(curItemPath)
521+
520522
// check decoded variant as well
521-
if (!items.has(curItemPath) && !opts.dev) {
522-
curItemPath = curDecodedItemPath
523+
if (!matchedItem && !opts.dev) {
524+
matchedItem = items.has(curItemPath)
525+
if (matchedItem) curItemPath = curDecodedItemPath
526+
else {
527+
// x-ref: https://github.com/vercel/next.js/issues/54008
528+
// There're cases that urls get decoded before requests, we should support both encoded and decoded ones.
529+
// e.g. nginx could decode the proxy urls, the below ones should be treated as the same:
530+
// decoded version: `/_next/static/chunks/pages/blog/[slug]-d4858831b91b69f6.js`
531+
// encoded version: `/_next/static/chunks/pages/blog/%5Bslug%5D-d4858831b91b69f6.js`
532+
try {
533+
// encode the special characters in the path and retrieve again to determine if path exists.
534+
const encodedCurItemPath = encodeURI(curItemPath)
535+
matchedItem = items.has(encodedCurItemPath)
536+
} catch {}
537+
}
523538
}
524-
const matchedItem = items.has(curItemPath)
525539

526540
if (matchedItem || opts.dev) {
527541
let fsPath: string | undefined
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,78 @@
1-
import { createNext } from 'e2e-utils'
2-
import { NextInstance } from 'test/lib/next-modes/base'
3-
import { renderViaHTTP } from 'next-test-utils'
4-
import cheerio from 'cheerio'
5-
import webdriver from 'next-webdriver'
1+
import { createNextDescribe } from 'e2e-utils'
62

7-
describe('Dynamic Route Interpolation', () => {
8-
let next: NextInstance
9-
10-
beforeAll(async () => {
11-
next = await createNext({
12-
files: {
13-
'pages/blog/[slug].js': `
14-
import Link from "next/link"
15-
import { useRouter } from "next/router"
16-
17-
export function getServerSideProps({ params }) {
18-
return { props: { slug: params.slug, now: Date.now() } }
19-
}
3+
createNextDescribe(
4+
'Dynamic Route Interpolation',
5+
{
6+
files: __dirname,
7+
},
8+
({ next, isNextStart }) => {
9+
it('should work', async () => {
10+
const $ = await next.render$('/blog/a')
11+
expect($('#slug').text()).toBe('a')
12+
})
2013

21-
export default function Page(props) {
22-
const router = useRouter()
23-
return (
24-
<>
25-
<p id="slug">{props.slug}</p>
26-
<Link id="now" href={router.asPath}>
27-
{props.now}
28-
</Link>
29-
</>
30-
)
31-
}
32-
`,
14+
it('should work with parameter itself', async () => {
15+
const $ = await next.render$('/blog/[slug]')
16+
expect($('#slug').text()).toBe('[slug]')
17+
})
3318

34-
'pages/api/dynamic/[slug].js': `
35-
export default function Page(req, res) {
36-
const { slug } = req.query
37-
res.end('slug: ' + slug)
38-
}
39-
`,
40-
},
41-
dependencies: {},
19+
it('should work with brackets', async () => {
20+
const $ = await next.render$('/blog/[abc]')
21+
expect($('#slug').text()).toBe('[abc]')
4222
})
43-
})
44-
afterAll(() => next.destroy())
4523

46-
it('should work', async () => {
47-
const html = await renderViaHTTP(next.url, '/blog/a')
48-
const $ = cheerio.load(html)
49-
expect($('#slug').text()).toBe('a')
50-
})
24+
it('should work with parameter itself in API routes', async () => {
25+
const text = await next.render('/api/dynamic/[slug]')
26+
expect(text).toBe('slug: [slug]')
27+
})
5128

52-
it('should work with parameter itself', async () => {
53-
const html = await renderViaHTTP(next.url, '/blog/[slug]')
54-
const $ = cheerio.load(html)
55-
expect($('#slug').text()).toBe('[slug]')
56-
})
29+
it('should work with brackets in API routes', async () => {
30+
const text = await next.render('/api/dynamic/[abc]')
31+
expect(text).toBe('slug: [abc]')
32+
})
5733

58-
it('should work with brackets', async () => {
59-
const html = await renderViaHTTP(next.url, '/blog/[abc]')
60-
const $ = cheerio.load(html)
61-
expect($('#slug').text()).toBe('[abc]')
62-
})
34+
it('should bust data cache', async () => {
35+
const browser = await next.browser('/blog/login')
36+
await browser.elementById('now').click() // fetch data once
37+
const text = await browser.elementById('now').text()
38+
await browser.elementById('now').click() // fetch data again
39+
await browser.waitForElementByCss(`#now:not(:text("${text}"))`)
40+
await browser.close()
41+
})
6342

64-
it('should work with parameter itself in API routes', async () => {
65-
const text = await renderViaHTTP(next.url, '/api/dynamic/[slug]')
66-
expect(text).toBe('slug: [slug]')
67-
})
43+
it('should bust data cache with symbol', async () => {
44+
const browser = await next.browser('/blog/@login')
45+
await browser.elementById('now').click() // fetch data once
46+
const text = await browser.elementById('now').text()
47+
await browser.elementById('now').click() // fetch data again
48+
await browser.waitForElementByCss(`#now:not(:text("${text}"))`)
49+
await browser.close()
50+
})
6851

69-
it('should work with brackets in API routes', async () => {
70-
const text = await renderViaHTTP(next.url, '/api/dynamic/[abc]')
71-
expect(text).toBe('slug: [abc]')
72-
})
52+
if (isNextStart) {
53+
it('should support both encoded and decoded nextjs reserved path convention characters in path', async () => {
54+
const $ = await next.render$('/blog/123')
55+
let pagePathScriptSrc
56+
for (const script of $('script').toArray()) {
57+
const { src } = script.attribs
58+
if (src.includes('slug') && src.includes('pages/blog')) {
59+
pagePathScriptSrc = src
60+
break
61+
}
62+
}
7363

74-
it('should bust data cache', async () => {
75-
const browser = await webdriver(next.url, '/blog/login')
76-
await browser.elementById('now').click() // fetch data once
77-
const text = await browser.elementById('now').text()
78-
await browser.elementById('now').click() // fetch data again
79-
await browser.waitForElementByCss(`#now:not(:text("${text}"))`)
80-
await browser.close()
81-
})
64+
// e.g. /_next/static/chunks/pages/blog/%5Bslug%5D-3d2fedc300f04305.js
65+
const { status: encodedPathReqStatus } = await next.fetch(
66+
pagePathScriptSrc
67+
)
68+
// e.g. /_next/static/chunks/pages/blog/[slug]-3d2fedc300f04305.js
69+
const { status: decodedPathReqStatus } = await next.fetch(
70+
decodeURI(pagePathScriptSrc)
71+
)
8272

83-
it('should bust data cache with symbol', async () => {
84-
const browser = await webdriver(next.url, '/blog/@login')
85-
await browser.elementById('now').click() // fetch data once
86-
const text = await browser.elementById('now').text()
87-
await browser.elementById('now').click() // fetch data again
88-
await browser.waitForElementByCss(`#now:not(:text("${text}"))`)
89-
await browser.close()
90-
})
91-
})
73+
expect(encodedPathReqStatus).toBe(200)
74+
expect(decodedPathReqStatus).toBe(200)
75+
})
76+
}
77+
}
78+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default function Page(req, res) {
2+
const { slug } = req.query
3+
res.end('slug: ' + slug)
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Link from 'next/link'
2+
import { useRouter } from 'next/router'
3+
4+
export function getServerSideProps({ params }) {
5+
return { props: { slug: params.slug, now: Date.now() } }
6+
}
7+
8+
export default function Page(props) {
9+
const router = useRouter()
10+
return (
11+
<>
12+
<p id="slug">{props.slug}</p>
13+
<Link id="now" href={router.asPath}>
14+
{props.now}
15+
</Link>
16+
</>
17+
)
18+
}

0 commit comments

Comments
 (0)