Skip to content

Commit 89ca0d1

Browse files
authored
Update to use getDataHref in fetchNextData (#14667)
This updates `fetchNextData` to re-use the `getDataHref` function from `page-loader` which has more verbose handling to ensure the correct `/_next/data` URL is built. Re-using this logic ensures the `/_next/data` URL can still be built even when a mismatching `href` and `as` value is provided to `next/link`. This also fixes a case in `getDataHref` where optional values that weren't provided would fail to build the data href since the check requiring the param be present while interpolating the route values hasn't been updated to allow missing params for optional values. An additional test case has been added to the prerender suite to ensure the `/_next/data` URL is built correctly when mismatching `href` and `as` values are provided x-ref: #14536 x-ref: #9081 (reply in thread) Closes: #14668
1 parent b8a30ba commit 89ca0d1

File tree

6 files changed

+92
-54
lines changed

6 files changed

+92
-54
lines changed

packages/next/client/page-loader.js

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,18 @@ export default class PageLoader {
109109
* @param {string} href the route href (file-system path)
110110
* @param {string} asPath the URL as shown in browser (virtual path); used for dynamic routes
111111
*/
112-
getDataHref(href, asPath) {
112+
getDataHref(href, asPath, ssg) {
113+
const { pathname: hrefPathname, query, search } = parse(href, true)
114+
const { pathname: asPathname } = parse(asPath)
115+
const route = normalizeRoute(hrefPathname)
116+
113117
const getHrefForSlug = (/** @type string */ path) => {
114118
const dataRoute = getAssetPathFromRoute(path, '.json')
115-
return `${this.assetPrefix}/_next/data/${this.buildId}${dataRoute}`
119+
return `${this.assetPrefix}/_next/data/${this.buildId}${dataRoute}${
120+
ssg ? '' : search || ''
121+
}`
116122
}
117123

118-
const { pathname: hrefPathname, query } = parse(href, true)
119-
const { pathname: asPathname } = parse(asPath)
120-
121-
const route = normalizeRoute(hrefPathname)
122-
123124
let isDynamic = isDynamicRoute(route),
124125
interpolatedRoute
125126
if (isDynamic) {
@@ -135,19 +136,19 @@ export default class PageLoader {
135136
interpolatedRoute = route
136137
if (
137138
!Object.keys(dynamicGroups).every((param) => {
138-
let value = dynamicMatches[param]
139+
let value = dynamicMatches[param] || ''
139140
const { repeat, optional } = dynamicGroups[param]
140141

141142
// support single-level catch-all
142143
// TODO: more robust handling for user-error (passing `/`)
143-
if (repeat && !Array.isArray(value)) value = [value]
144144
let replaced = `[${repeat ? '...' : ''}${param}]`
145145
if (optional) {
146-
replaced = `[${replaced}]`
146+
replaced = `${!value ? '/' : ''}[${replaced}]`
147147
}
148+
if (repeat && !Array.isArray(value)) value = [value]
148149

149150
return (
150-
param in dynamicMatches &&
151+
(optional || param in dynamicMatches) &&
151152
// Interpolate group into data URL if present
152153
(interpolatedRoute = interpolatedRoute.replace(
153154
replaced,
@@ -182,7 +183,7 @@ export default class PageLoader {
182183
// Check if the route requires a data file
183184
s.has(route) &&
184185
// Try to generate data href, noop when falsy
185-
(_dataHref = this.getDataHref(href, asPath)) &&
186+
(_dataHref = this.getDataHref(href, asPath, true)) &&
186187
// noop when data has already been prefetched (dedupe)
187188
!document.querySelector(
188189
`link[rel="${relPrefetch}"][href^="${_dataHref}"]`

packages/next/next-server/lib/router/router.ts

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { isDynamicRoute } from './utils/is-dynamic'
1616
import { getRouteMatcher } from './utils/route-matcher'
1717
import { getRouteRegex } from './utils/route-regex'
1818
import { normalizeTrailingSlash } from './normalize-trailing-slash'
19-
import getAssetPathFromRoute from './utils/get-asset-path-from-route'
2019

2120
const basePath = (process.env.__NEXT_ROUTER_BASEPATH as string) || ''
2221

@@ -108,39 +107,26 @@ type ComponentLoadCancel = (() => void) | null
108107
type HistoryMethod = 'replaceState' | 'pushState'
109108

110109
function fetchNextData(
111-
pathname: string,
112-
query: ParsedUrlQuery | null,
110+
dataHref: string,
113111
isServerRender: boolean,
114112
cb?: (...args: any) => any
115113
) {
116114
let attempts = isServerRender ? 3 : 1
117115
function getResponse(): Promise<any> {
118-
return fetch(
119-
formatWithValidation({
120-
pathname: addBasePath(
121-
// @ts-ignore __NEXT_DATA__
122-
`/_next/data/${__NEXT_DATA__.buildId}${getAssetPathFromRoute(
123-
pathname,
124-
'.json'
125-
)}`
126-
),
127-
query,
128-
}),
129-
{
130-
// Cookies are required to be present for Next.js' SSG "Preview Mode".
131-
// Cookies may also be required for `getServerSideProps`.
132-
//
133-
// > `fetch` won’t send cookies, unless you set the credentials init
134-
// > option.
135-
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
136-
//
137-
// > For maximum browser compatibility when it comes to sending &
138-
// > receiving cookies, always supply the `credentials: 'same-origin'`
139-
// > option instead of relying on the default.
140-
// https://github.com/github/fetch#caveats
141-
credentials: 'same-origin',
142-
}
143-
).then((res) => {
116+
return fetch(dataHref, {
117+
// Cookies are required to be present for Next.js' SSG "Preview Mode".
118+
// Cookies may also be required for `getServerSideProps`.
119+
//
120+
// > `fetch` won’t send cookies, unless you set the credentials init
121+
// > option.
122+
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
123+
//
124+
// > For maximum browser compatibility when it comes to sending &
125+
// > receiving cookies, always supply the `credentials: 'same-origin'`
126+
// > option instead of relying on the default.
127+
// https://github.com/github/fetch#caveats
128+
credentials: 'same-origin',
129+
}).then((res) => {
144130
if (!res.ok) {
145131
if (--attempts > 0 && res.status >= 500) {
146132
return getResponse()
@@ -669,11 +655,21 @@ export default class Router implements BaseRouter {
669655
}
670656
}
671657

658+
let dataHref: string | undefined
659+
660+
if (__N_SSG || __N_SSP) {
661+
dataHref = this.pageLoader.getDataHref(
662+
formatWithValidation({ pathname, query }),
663+
as,
664+
__N_SSG
665+
)
666+
}
667+
672668
return this._getData<RouteInfo>(() =>
673669
__N_SSG
674-
? this._getStaticData(as)
670+
? this._getStaticData(dataHref!)
675671
: __N_SSP
676-
? this._getServerData(as)
672+
? this._getServerData(dataHref!)
677673
: this.getInitialProps(
678674
Component,
679675
// we provide AppTree later so this needs to be `any`
@@ -843,23 +839,20 @@ export default class Router implements BaseRouter {
843839
})
844840
}
845841

846-
_getStaticData = (asPath: string): Promise<object> => {
847-
const pathname = prepareRoute(parse(asPath).pathname!)
842+
_getStaticData = (dataHref: string): Promise<object> => {
843+
const pathname = prepareRoute(parse(dataHref).pathname!)
848844

849845
return process.env.NODE_ENV === 'production' && this.sdc[pathname]
850-
? Promise.resolve(this.sdc[pathname])
846+
? Promise.resolve(this.sdc[dataHref])
851847
: fetchNextData(
852-
pathname,
853-
null,
848+
dataHref,
854849
this.isSsr,
855850
(data) => (this.sdc[pathname] = data)
856851
)
857852
}
858853

859-
_getServerData = (asPath: string): Promise<object> => {
860-
let { pathname, query } = parse(asPath, true)
861-
pathname = prepareRoute(pathname!)
862-
return fetchNextData(pathname, query, this.isSsr)
854+
_getServerData = (dataHref: string): Promise<object> => {
855+
return fetchNextData(dataHref, this.isSsr)
863856
}
864857

865858
getInitialProps(

test/integration/build-output/test/index.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ describe('Build Output', () => {
113113
expect(parseFloat(webpackSize) - 775).toBeLessThanOrEqual(0)
114114
expect(webpackSize.endsWith('B')).toBe(true)
115115

116-
expect(parseFloat(mainSize) - 6.3).toBeLessThanOrEqual(0)
116+
expect(parseFloat(mainSize) - 6.4).toBeLessThanOrEqual(0)
117117
expect(mainSize.endsWith('kB')).toBe(true)
118118

119119
expect(parseFloat(frameworkSize) - 41).toBeLessThanOrEqual(0)

test/integration/prerender/pages/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ const Page = ({ world, time }) => {
5252
<a id="to-nested-index">to nested index</a>
5353
</Link>
5454
<br />
55+
<Link href="/lang/[lang]/about?lang=en" as="/about">
56+
<a id="to-rewritten-ssg">to rewritten static path page</a>
57+
</Link>
58+
<br />
5559
<Link href="/catchall-optional/[[...slug]]" as="/catchall-optional">
5660
<a id="catchall-optional-root">to optional catchall root</a>
5761
</Link>

test/integration/prerender/pages/lang/[lang]/about.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export default ({ lang }) => <p>About: {lang}</p>
1+
export default ({ lang }) => <p id="about">About: {lang}</p>
22

33
export const getStaticProps = ({ params: { lang } }) => ({
44
props: {

test/integration/prerender/test/index.test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from 'next-test-utils'
2626
import webdriver from 'next-webdriver'
2727
import { dirname, join } from 'path'
28+
import url from 'url'
2829

2930
jest.setTimeout(1000 * 60 * 2)
3031
const appDir = join(__dirname, '..')
@@ -601,6 +602,45 @@ const runTests = (dev = false, isEmulatedServerless = false) => {
601602
const html = await renderViaHTTP(appPort, '/about')
602603
expect(html).toMatch(/About:.*?en/)
603604
})
605+
606+
it('should fetch /_next/data correctly with mismatched href and as', async () => {
607+
const browser = await webdriver(appPort, '/')
608+
609+
if (!dev) {
610+
await browser.eval(() =>
611+
document.querySelector('#to-rewritten-ssg').scrollIntoView()
612+
)
613+
614+
await check(
615+
async () => {
616+
const links = await browser.elementsByCss('link[rel=prefetch]')
617+
let found = false
618+
619+
for (const link of links) {
620+
const href = await link.getAttribute('href')
621+
const { pathname } = url.parse(href)
622+
623+
if (pathname.endsWith('/lang/en/about.json')) {
624+
found = true
625+
break
626+
}
627+
}
628+
return found
629+
},
630+
{
631+
test(result) {
632+
return result === true
633+
},
634+
}
635+
)
636+
}
637+
await browser.eval('window.beforeNav = "hi"')
638+
await browser.elementByCss('#to-rewritten-ssg').click()
639+
await browser.waitForElementByCss('#about')
640+
641+
expect(await browser.eval('window.beforeNav')).toBe('hi')
642+
expect(await browser.elementByCss('#about').text()).toBe('About: en')
643+
})
604644
}
605645

606646
if (dev) {

0 commit comments

Comments
 (0)