Skip to content

Commit

Permalink
Relax encoding on dynamic prerendered routes (#14717)
Browse files Browse the repository at this point in the history
It should be enough to encode the characters that `path-to-regexp` uses as path delimiters (`/#?`).

Fixes #14691
  • Loading branch information
Janpot authored Jul 13, 2020
1 parent 272080c commit c9492a8
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 6 deletions.
5 changes: 3 additions & 2 deletions packages/next/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import prettyBytes from '../lib/pretty-bytes'
import { recursiveReadDir } from '../lib/recursive-readdir'
import { getRouteMatcher, getRouteRegex } from '../next-server/lib/router/utils'
import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic'
import escapePathDelimiters from '../next-server/lib/router/utils/escape-path-delimiters'
import { findPageFile } from '../server/lib/find-page-file'
import { GetStaticPaths } from 'next/types'
import { denormalizePagePath } from '../next-server/server/normalize-page-path'
Expand Down Expand Up @@ -631,8 +632,8 @@ export async function buildStaticPaths(
.replace(
replaced,
repeat
? (paramValue as string[]).map(encodeURIComponent).join('/')
: encodeURIComponent(paramValue as string)
? (paramValue as string[]).map(escapePathDelimiters).join('/')
: escapePathDelimiters(paramValue as string)
)
.replace(/(?!^)\/$/, '')
})
Expand Down
5 changes: 3 additions & 2 deletions packages/next/client/page-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import mitt from '../next-server/lib/mitt'
import { isDynamicRoute } from './../next-server/lib/router/utils/is-dynamic'
import { getRouteMatcher } from './../next-server/lib/router/utils/route-matcher'
import { getRouteRegex } from './../next-server/lib/router/utils/route-regex'
import escapePathDelimiters from '../next-server/lib/router/utils/escape-path-delimiters'
import getAssetPathFromRoute from './../next-server/lib/router/utils/get-asset-path-from-route'

function hasRel(rel, link) {
Expand Down Expand Up @@ -153,8 +154,8 @@ export default class PageLoader {
(interpolatedRoute = interpolatedRoute.replace(
replaced,
repeat
? value.map(encodeURIComponent).join('/')
: encodeURIComponent(value)
? value.map(escapePathDelimiters).join('/')
: escapePathDelimiters(value)
))
)
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// escape delimiters used by path-to-regexp
export default function escapePathDelimiters(segment: string): string {
return segment.replace(/[/#?]/g, (char: string) => encodeURIComponent(char))
}
15 changes: 13 additions & 2 deletions test/integration/prerender/pages/catchall-explicit/[...slug].js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Link from 'next/link'

export async function getStaticProps({ params: { slug } }) {
if (slug[0] === 'delayby3s') {
await new Promise((resolve) => setTimeout(resolve, 3000))
Expand All @@ -18,13 +20,22 @@ export async function getStaticPaths() {
'/catchall-explicit/second',
{ params: { slug: ['another', 'value'] } },
'/catchall-explicit/hello/another',
'/catchall-explicit/[first]/[second]',
{ params: { slug: ['[third]', '[fourth]'] } },
],
fallback: false,
}
}

export default ({ slug }) => {
export default function Page({ slug }) {
// Important to not check for `slug` existence (testing that build does not
// render fallback version and error)
return <p id="catchall">Hi {slug.join(' ')}</p>
return (
<>
<p id="catchall">Hi {slug.join(' ')}</p>{' '}
<Link href="/">
<a id="home">to home</a>
</Link>
</>
)
}
27 changes: 27 additions & 0 deletions test/integration/prerender/pages/dynamic/[slug].js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Link from 'next/link'

export async function getStaticProps({ params: { slug } }) {
return {
props: { slug },
}
}

export async function getStaticPaths() {
return {
paths: [{ params: { slug: '[first]' } }, '/dynamic/[second]'],
fallback: false,
}
}

export default ({ slug }) => {
// Important to not check for `slug` existence (testing that build does not
// render fallback version and error)
return (
<>
<p id="param">Hi {slug}!</p>{' '}
<Link href="/">
<a id="home">to home</a>
</Link>
</>
)
}
24 changes: 24 additions & 0 deletions test/integration/prerender/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ const Page = ({ world, time }) => {
<Link href="/catchall-optional/[[...slug]]" as="/catchall-optional/value">
<a id="catchall-optional-value">to optional catchall page /value</a>
</Link>
<br />
<Link href="/dynamic/[slug]" as="/dynamic/[first]">
<a id="dynamic-first">to dynamic [first] page</a>
</Link>
<Link href="/dynamic/[slug]" as="/dynamic/[second]">
<a id="dynamic-second">to dynamic [second] page</a>
</Link>
<br />
<Link
href="/catchall-explicit/[...slug]"
as="/catchall-explicit/[first]/[second]"
>
<a id="catchall-explicit-string">
to catchall-explicit [first]/[second] page
</a>
</Link>
<Link
href="/catchall-explicit/[...slug]"
as="/catchall-explicit/[third]/[fourth]"
>
<a id="catchall-explicit-object">
to catchall-explicit [third]/[fourth] page
</a>
</Link>
</>
)
}
Expand Down
148 changes: 148 additions & 0 deletions test/integration/prerender/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ const expectedManifestRoutes = () => ({
initialRevalidateSeconds: 1,
srcRoute: '/catchall-explicit/[...slug]',
},
'/catchall-explicit/[first]/[second]': {
dataRoute: `/_next/data/${buildId}/catchall-explicit/[first]/[second].json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall-explicit/[...slug]',
},
'/catchall-explicit/[third]/[fourth]': {
dataRoute: `/_next/data/${buildId}/catchall-explicit/[third]/[fourth].json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall-explicit/[...slug]',
},
'/catchall-optional': {
dataRoute: `/_next/data/${buildId}/catchall-optional.json`,
initialRevalidateSeconds: false,
Expand All @@ -142,6 +152,16 @@ const expectedManifestRoutes = () => ({
initialRevalidateSeconds: false,
srcRoute: null,
},
'/dynamic/[first]': {
dataRoute: `/_next/data/${buildId}/dynamic/[first].json`,
initialRevalidateSeconds: false,
srcRoute: '/dynamic/[slug]',
},
'/dynamic/[second]': {
dataRoute: `/_next/data/${buildId}/dynamic/[second].json`,
initialRevalidateSeconds: false,
srcRoute: '/dynamic/[slug]',
},
'/index': {
dataRoute: `/_next/data/${buildId}/index/index.json`,
initialRevalidateSeconds: false,
Expand Down Expand Up @@ -307,6 +327,50 @@ const navigateTest = (dev = false) => {
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')

// go to /dynamic/[first]
await browser.elementByCss('#dynamic-first').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('#param').text()
expect(text).toMatch(/Hi \[first\]!/)
expect(await browser.eval('window.didTransition')).toBe(1)

// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')

// go to /dynamic/[second]
await browser.elementByCss('#dynamic-second').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('#param').text()
expect(text).toMatch(/Hi \[second\]!/)
expect(await browser.eval('window.didTransition')).toBe(1)

// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')

// go to /catchall-explicit/[first]/[second]
await browser.elementByCss('#catchall-explicit-string').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('#catchall').text()
expect(text).toMatch(/Hi \[first\] \[second\]/)
expect(await browser.eval('window.didTransition')).toBe(1)

// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')

// go to /catchall-explicit/[first]/[second]
await browser.elementByCss('#catchall-explicit-object').click()
await browser.waitForElementByCss('#home')
text = await browser.elementByCss('#catchall').text()
expect(text).toMatch(/Hi \[third\] \[fourth\]/)
expect(await browser.eval('window.didTransition')).toBe(1)

// go to /
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#comment-1')

// go to /catchall-optional/value
await browser.elementByCss('#catchall-optional-value').click()
await browser.waitForElementByCss('#home')
Expand Down Expand Up @@ -460,6 +524,68 @@ const runTests = (dev = false, isEmulatedServerless = false) => {
).toBe(true)
})

it('should SSR dynamic page with brackets in param as object', async () => {
const html = await renderViaHTTP(appPort, '/dynamic/[first]')
const $ = cheerio.load(html)
expect($('#param').text()).toMatch(/Hi \[first\]!/)
})

it('should navigate to dynamic page with brackets in param as object', async () => {
const browser = await webdriver(appPort, '/')
await browser.elementByCss('#dynamic-first').click()
await browser.waitForElementByCss('#param')
const value = await browser.elementByCss('#param').text()
expect(value).toMatch(/Hi \[first\]!/)
})

it('should SSR dynamic page with brackets in param as string', async () => {
const html = await renderViaHTTP(appPort, '/dynamic/[second]')
const $ = cheerio.load(html)
expect($('#param').text()).toMatch(/Hi \[second\]!/)
})

it('should navigate to dynamic page with brackets in param as string', async () => {
const browser = await webdriver(appPort, '/')
await browser.elementByCss('#dynamic-second').click()
await browser.waitForElementByCss('#param')
const value = await browser.elementByCss('#param').text()
expect(value).toMatch(/Hi \[second\]!/)
})

it('should SSR catch-all page with brackets in param as string', async () => {
const html = await renderViaHTTP(
appPort,
'/catchall-explicit/[first]/[second]'
)
const $ = cheerio.load(html)
expect($('#catchall').text()).toMatch(/Hi \[first\] \[second\]/)
})

it('should navigate to catch-all page with brackets in param as string', async () => {
const browser = await webdriver(appPort, '/')
await browser.elementByCss('#catchall-explicit-string').click()
await browser.waitForElementByCss('#catchall')
const value = await browser.elementByCss('#catchall').text()
expect(value).toMatch(/Hi \[first\] \[second\]/)
})

it('should SSR catch-all page with brackets in param as object', async () => {
const html = await renderViaHTTP(
appPort,
'/catchall-explicit/[third]/[fourth]'
)
const $ = cheerio.load(html)
expect($('#catchall').text()).toMatch(/Hi \[third\] \[fourth\]/)
})

it('should navigate to catch-all page with brackets in param as object', async () => {
const browser = await webdriver(appPort, '/')
await browser.elementByCss('#catchall-explicit-object').click()
await browser.waitForElementByCss('#catchall')
const value = await browser.elementByCss('#catchall').text()
expect(value).toMatch(/Hi \[third\] \[fourth\]/)
})

// TODO: dev currently renders this page as blocking, meaning it shows the
// server error instead of continously retrying. Do we want to change this?
if (!dev) {
Expand Down Expand Up @@ -1016,6 +1142,20 @@ const runTests = (dev = false, isEmulatedServerless = false) => {
),
page: '/default-revalidate',
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
buildId
)}\\/dynamic\\/([^\\/]+?)\\.json$`
),
namedDataRouteRegex: `^/_next/data/${escapeRegex(
buildId
)}/dynamic/(?<slug>[^/]+?)\\.json$`,
page: '/dynamic/[slug]',
routeKeys: {
slug: 'slug',
},
},
{
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapeRegex(
Expand Down Expand Up @@ -1125,6 +1265,14 @@ const runTests = (dev = false, isEmulatedServerless = false) => {
'^\\/blog\\/([^\\/]+?)\\/([^\\/]+?)(?:\\/)?$'
),
},
'/dynamic/[slug]': {
dataRoute: `/_next/data/${buildId}/dynamic/[slug].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/dynamic\\/([^\\/]+?)\\.json$`
),
fallback: false,
routeRegex: normalizeRegEx(`^\\/dynamic\\/([^\\/]+?)(?:\\/)?$`),
},
'/fallback-only/[slug]': {
dataRoute: `/_next/data/${buildId}/fallback-only/[slug].json`,
dataRouteRegex: normalizeRegEx(
Expand Down

0 comments on commit c9492a8

Please sign in to comment.