Skip to content

Commit cfb4ac5

Browse files
authored
fix: prevent incorrect searchParams being applied on certain navs (#76914)
When the router returns an aliased prefetch for URLs to the same path but with different search params, there's the possibility that we trigger an MPA-style navigation if the path doesn't contain a valid flight payload (eg: a route handler, a `pages` router page, or an external URL). The logic to bail out of the alias handling was happening _after_ this, which meant the incorrect searchParams would be applied to the final URL. This ensures that if we bail out of applying the alias (eg because it's not a valid RSC payload, or the aliased entry has no reusable loading segment), then we re-run the navigation with a non-aliased entry. Fixes #75318 Closes NDX-804
1 parent 56a6c86 commit cfb4ac5

File tree

7 files changed

+131
-20
lines changed

7 files changed

+131
-20
lines changed

packages/next/src/client/components/router-reducer/aliased-prefetch-navigations.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import type { Mutable, ReadonlyReducerState } from './router-reducer-types'
2525
*/
2626
export function handleAliasedPrefetchEntry(
2727
state: ReadonlyReducerState,
28-
flightData: NormalizedFlightData[],
28+
flightData: string | NormalizedFlightData[],
2929
url: URL,
3030
mutable: Mutable
3131
) {
@@ -34,6 +34,10 @@ export function handleAliasedPrefetchEntry(
3434
const href = createHrefFromUrl(url)
3535
let applied
3636

37+
if (typeof flightData === 'string') {
38+
return false
39+
}
40+
3741
for (const normalizedFlightData of flightData) {
3842
// If the segment doesn't have a loading component, we don't need to do anything.
3943
if (!hasLoadingComponentInSeedData(normalizedFlightData.seedData)) {

packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,24 @@ export function navigateReducer(
234234
isFirstRead = true
235235
}
236236

237+
if (prefetchValues.aliased) {
238+
const result = handleAliasedPrefetchEntry(
239+
state,
240+
flightData,
241+
url,
242+
mutable
243+
)
244+
245+
// We didn't return new router state because we didn't apply the aliased entry for some reason.
246+
// We'll re-invoke the navigation handler but ensure that we don't attempt to use the aliased entry. This
247+
// will create an on-demand prefetch entry.
248+
if (result === false) {
249+
return navigateReducer(state, { ...action, allowAliasing: false })
250+
}
251+
252+
return result
253+
}
254+
237255
// Handle case when navigating to page in `pages` from `app`
238256
if (typeof flightData === 'string') {
239257
return handleExternalUrl(state, mutable, flightData, pendingPush)
@@ -259,24 +277,6 @@ export function navigateReducer(
259277
return handleMutable(state, mutable)
260278
}
261279

262-
if (prefetchValues.aliased) {
263-
const result = handleAliasedPrefetchEntry(
264-
state,
265-
flightData,
266-
url,
267-
mutable
268-
)
269-
270-
// We didn't return new router state because we didn't apply the aliased entry for some reason.
271-
// We'll re-invoke the navigation handler but ensure that we don't attempt to use the aliased entry. This
272-
// will create an on-demand prefetch entry.
273-
if (result === false) {
274-
return navigateReducer(state, { ...action, allowAliasing: false })
275-
}
276-
277-
return result
278-
}
279-
280280
let currentTree = state.tree
281281
let currentCache = state.cache
282282
let scrollableSegments: FlightSegmentPath[] = []
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Link from 'next/link'
2+
3+
export default function Page() {
4+
return (
5+
<ul>
6+
<li>
7+
<Link href="/non-existent-page?id=1">/non-existent-page?id=1</Link>
8+
</li>
9+
<li>
10+
<Link href="/non-existent-page?id=2">/non-existent-page?id=2</Link>
11+
</li>
12+
<li>
13+
<Link href="/pages-dir?param=1">/pages-dir?param=1</Link>
14+
</li>
15+
<li>
16+
<Link href="/pages-dir?param=2">/pages-dir?param=2</Link>
17+
</li>
18+
<li>
19+
<Link href="/route-handler?param=1">/route-handler?param=1</Link>
20+
</li>
21+
<li>
22+
<Link href="/route-handler?param=2">/route-handler?param=2</Link>
23+
</li>
24+
</ul>
25+
)
26+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
3+
export async function GET(req: NextRequest) {
4+
const param = req.nextUrl.searchParams.get('param') as string
5+
return NextResponse.json({ param })
6+
}

test/e2e/app-dir/searchparams-reuse-loading/app/search/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ export default function SearchLayout({
99
children: React.ReactNode
1010
}) {
1111
let searchParams = useSearchParams()
12-
return <Fragment key={searchParams.get('q')}>{children}</Fragment>
12+
return <Fragment key={searchParams?.get('q')}>{children}</Fragment>
1313
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { useRouter } from 'next/router'
2+
3+
export default function Page() {
4+
const router = useRouter()
5+
return <div>Hello from pages dir! {router.query.param}</div>
6+
}

test/e2e/app-dir/searchparams-reuse-loading/searchparams-reuse-loading.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,75 @@ describe('searchparams-reuse-loading', () => {
8787
)
8888
})
8989

90+
describe('when aliasing is skipped', () => {
91+
it('should work for not found pages', async () => {
92+
const browser = await next.browser('/mpa-navs')
93+
await browser.elementByCss("[href='/non-existent-page?id=1']").click()
94+
95+
// the first link would have been the "aliased" entry since it was prefetched first. Validate that it's the correct URL
96+
await retry(async () => {
97+
expect(await browser.url()).toContain('/non-existent-page?id=1')
98+
})
99+
100+
expect(await browser.elementByCss('h2').text()).toBe(
101+
'This page could not be found.'
102+
)
103+
104+
// The other link would have attempted to use the aliased entry. Ensure the browser ends up on the correct page
105+
await browser.loadPage(`${next.url}/mpa-navs`)
106+
await retry(async () => {
107+
expect(await browser.url()).toContain('/mpa-navs')
108+
})
109+
await browser.elementByCss("[href='/non-existent-page?id=2']").click()
110+
await retry(async () => {
111+
expect(await browser.url()).toContain('/non-existent-page?id=2')
112+
})
113+
expect(await browser.elementByCss('h2').text()).toBe(
114+
'This page could not be found.'
115+
)
116+
})
117+
118+
it('should work for route handlers', async () => {
119+
const browser = await next.browser('/mpa-navs')
120+
await browser.elementByCss("[href='/route-handler?param=1']").click()
121+
await retry(async () => {
122+
expect(await browser.url()).toContain('/route-handler?param=1')
123+
})
124+
125+
await browser.loadPage(`${next.url}/mpa-navs`)
126+
await retry(async () => {
127+
expect(await browser.url()).toContain('/mpa-navs')
128+
})
129+
130+
await browser.elementByCss("[href='/route-handler?param=2']").click()
131+
await retry(async () => {
132+
expect(await browser.url()).toContain('/route-handler?param=2')
133+
})
134+
})
135+
136+
it('should work for navigating to pages dir', async () => {
137+
const browser = await next.browser('/mpa-navs')
138+
await browser.elementByCss("[href='/pages-dir?param=1']").click()
139+
140+
await retry(async () => {
141+
expect(await browser.elementByCss('body').text()).toContain(
142+
'Hello from pages dir! 1'
143+
)
144+
expect(await browser.url()).toContain('/pages-dir?param=1')
145+
})
146+
147+
await browser.loadPage(`${next.url}/mpa-navs`)
148+
149+
await browser.elementByCss("[href='/pages-dir?param=2']").click()
150+
await retry(async () => {
151+
expect(await browser.elementByCss('body').text()).toContain(
152+
'Hello from pages dir! 2'
153+
)
154+
expect(await browser.url()).toContain('/pages-dir?param=2')
155+
})
156+
})
157+
})
158+
90159
// Dev doesn't perform prefetching, so this test is skipped, as it relies on intercepting
91160
// prefetch network requests.
92161
if (!isNextDev) {

0 commit comments

Comments
 (0)