Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support both decoded and encoded url requests of conventioned files #56187

Merged
merged 7 commits into from
Oct 2, 2023
20 changes: 17 additions & 3 deletions packages/next/src/server/lib/router-utils/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,11 +517,25 @@ export async function setupFsCheck(opts: {
} catch {}
}

let matchedItem = items.has(curItemPath)

// check decoded variant as well
if (!items.has(curItemPath) && !opts.dev) {
curItemPath = curDecodedItemPath
if (!matchedItem && !opts.dev) {
matchedItem = items.has(curItemPath)
if (matchedItem) curItemPath = curDecodedItemPath
else {
// x-ref: https://github.com/vercel/next.js/issues/54008
// There're cases that urls get decoded before requests, we should support both encoded and decoded ones.
// e.g. nginx could decode the proxy urls, the below ones should be treated as the same:
// decoded version: `/_next/static/chunks/pages/blog/[slug]-d4858831b91b69f6.js`
// encoded version: `/_next/static/chunks/pages/blog/%5Bslug%5D-d4858831b91b69f6.js`
try {
// encode the special characters in the path and retrieve again to determine if path exists.
const encodedCurItemPath = encodeURI(curItemPath)
omarmciver marked this conversation as resolved.
Show resolved Hide resolved
matchedItem = items.has(encodedCurItemPath)
} catch {}
}
}
const matchedItem = items.has(curItemPath)

if (matchedItem || opts.dev) {
let fsPath: string | undefined
Expand Down
147 changes: 67 additions & 80 deletions test/e2e/dynamic-route-interpolation/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,91 +1,78 @@
import { createNext } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { renderViaHTTP } from 'next-test-utils'
import cheerio from 'cheerio'
import webdriver from 'next-webdriver'
import { createNextDescribe } from 'e2e-utils'

describe('Dynamic Route Interpolation', () => {
let next: NextInstance

beforeAll(async () => {
next = await createNext({
files: {
'pages/blog/[slug].js': `
import Link from "next/link"
import { useRouter } from "next/router"

export function getServerSideProps({ params }) {
return { props: { slug: params.slug, now: Date.now() } }
}
createNextDescribe(
'Dynamic Route Interpolation',
{
files: __dirname,
},
({ next, isNextStart }) => {
it('should work', async () => {
const $ = await next.render$('/blog/a')
expect($('#slug').text()).toBe('a')
})

export default function Page(props) {
const router = useRouter()
return (
<>
<p id="slug">{props.slug}</p>
<Link id="now" href={router.asPath}>
{props.now}
</Link>
</>
)
}
`,
it('should work with parameter itself', async () => {
const $ = await next.render$('/blog/[slug]')
expect($('#slug').text()).toBe('[slug]')
})

'pages/api/dynamic/[slug].js': `
export default function Page(req, res) {
const { slug } = req.query
res.end('slug: ' + slug)
}
`,
},
dependencies: {},
it('should work with brackets', async () => {
const $ = await next.render$('/blog/[abc]')
expect($('#slug').text()).toBe('[abc]')
})
})
afterAll(() => next.destroy())

it('should work', async () => {
const html = await renderViaHTTP(next.url, '/blog/a')
const $ = cheerio.load(html)
expect($('#slug').text()).toBe('a')
})
it('should work with parameter itself in API routes', async () => {
const text = await next.render('/api/dynamic/[slug]')
expect(text).toBe('slug: [slug]')
})

it('should work with parameter itself', async () => {
const html = await renderViaHTTP(next.url, '/blog/[slug]')
const $ = cheerio.load(html)
expect($('#slug').text()).toBe('[slug]')
})
it('should work with brackets in API routes', async () => {
const text = await next.render('/api/dynamic/[abc]')
expect(text).toBe('slug: [abc]')
})

it('should work with brackets', async () => {
const html = await renderViaHTTP(next.url, '/blog/[abc]')
const $ = cheerio.load(html)
expect($('#slug').text()).toBe('[abc]')
})
it('should bust data cache', async () => {
const browser = await next.browser('/blog/login')
await browser.elementById('now').click() // fetch data once
const text = await browser.elementById('now').text()
await browser.elementById('now').click() // fetch data again
await browser.waitForElementByCss(`#now:not(:text("${text}"))`)
await browser.close()
})

it('should work with parameter itself in API routes', async () => {
const text = await renderViaHTTP(next.url, '/api/dynamic/[slug]')
expect(text).toBe('slug: [slug]')
})
it('should bust data cache with symbol', async () => {
const browser = await next.browser('/blog/@login')
await browser.elementById('now').click() // fetch data once
const text = await browser.elementById('now').text()
await browser.elementById('now').click() // fetch data again
await browser.waitForElementByCss(`#now:not(:text("${text}"))`)
await browser.close()
})

it('should work with brackets in API routes', async () => {
const text = await renderViaHTTP(next.url, '/api/dynamic/[abc]')
expect(text).toBe('slug: [abc]')
})
if (isNextStart) {
it('should support both encoded and decoded nextjs reserved path convention characters in path', async () => {
const $ = await next.render$('/blog/123')
let pagePathScriptSrc
for (const script of $('script').toArray()) {
const { src } = script.attribs
if (src.includes('slug') && src.includes('pages/blog')) {
pagePathScriptSrc = src
break
}
}

it('should bust data cache', async () => {
const browser = await webdriver(next.url, '/blog/login')
await browser.elementById('now').click() // fetch data once
const text = await browser.elementById('now').text()
await browser.elementById('now').click() // fetch data again
await browser.waitForElementByCss(`#now:not(:text("${text}"))`)
await browser.close()
})
// e.g. /_next/static/chunks/pages/blog/%5Bslug%5D-3d2fedc300f04305.js
const { status: encodedPathReqStatus } = await next.fetch(
pagePathScriptSrc
)
// e.g. /_next/static/chunks/pages/blog/[slug]-3d2fedc300f04305.js
const { status: decodedPathReqStatus } = await next.fetch(
decodeURI(pagePathScriptSrc)
)

it('should bust data cache with symbol', async () => {
const browser = await webdriver(next.url, '/blog/@login')
await browser.elementById('now').click() // fetch data once
const text = await browser.elementById('now').text()
await browser.elementById('now').click() // fetch data again
await browser.waitForElementByCss(`#now:not(:text("${text}"))`)
await browser.close()
})
})
expect(encodedPathReqStatus).toBe(200)
expect(decodedPathReqStatus).toBe(200)
})
}
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default function Page(req, res) {
const { slug } = req.query
res.end('slug: ' + slug)
}
18 changes: 18 additions & 0 deletions test/e2e/dynamic-route-interpolation/pages/blog/[slug].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Link from 'next/link'
import { useRouter } from 'next/router'

export function getServerSideProps({ params }) {
return { props: { slug: params.slug, now: Date.now() } }
}

export default function Page(props) {
const router = useRouter()
return (
<>
<p id="slug">{props.slug}</p>
<Link id="now" href={router.asPath}>
{props.now}
</Link>
</>
)
}
Loading