Files
Andrew ClarkandGitHub a7877d776b Bugfix: Incorrect search params stored in cache (#94144)
The route cache write was keying entries on `renderedSearch` — the
search string the server actually used to render the page — but lookups
use the requested URL's search component. These two are not equivalent:
for any search-invariant route (e.g. a static page), `renderedSearch` is
`''` regardless of what the request URL carried.

The result was that a request for `/a?foo=bar` against a static page
parked the cache entry at vary path `(/a, '', null)` — the same slot a
clean `/a` lookup keys to. The entry's stored `canonicalUrl` came from
the requesting URL (`/a?foo=bar`), so a later `router.push('/a')` or
`router.replace('/a')` would hit that slot and restore the stale search
string into the browser address bar, `window.location`, and
`useSearchParams()`.

The fix keys cache writes on the request URL's search, not
`renderedSearch`. Entries now sit at the slot keyed by the request that
wrote them, so a lookup for a different search variant doesn't find
them.

Regression test covers the reproduction from the user repo linked in the
bug reports.

Closes #91658
Closes #92187

<!-- NEXT_JS_LLM_PR -->
2026-05-27 11:31:47 +02:00

49 lines
1.7 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
describe('segment cache - stale search params on replace regression', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
if (isNextDev) {
test('skipped in dev mode', () => {})
return
}
// Regression test for https://github.com/vercel/next.js/issues/91658.
//
// Reproduction is taken directly from the user's report and from their
// minimal repro at https://github.com/dlehmhus/next-router-replace-bug:
//
// 1. Build and start the application.
// 2. Open `/?query=param` directly in the browser (full page load).
// 3. Click `<Link href="/dummy-page-1">` — navigates to /dummy-page-1.
// 4. Click `<Link href="/dummy-page-2">` — navigates to /dummy-page-2.
// 5. Click the button that calls `router.replace('/')`.
//
// Expected: URL is `/` with no search string.
// Buggy: URL becomes `/?query=param` — the search string from the
// original page load is restored.
it('router.replace to a clean URL clears the search params from the initial page load', async () => {
const browser = await next.browser('/?query=param')
await browser.waitForElementByCss('#home')
await browser.elementById('link-to-dummy-1').click()
await browser.waitForElementByCss('#dummy-page-1')
await browser.elementById('link-to-dummy-2').click()
await browser.waitForElementByCss('#dummy-page-2')
await browser.elementById('go-home').click()
await browser.waitForElementByCss('#home')
await retry(async () => {
const url = new URL(await browser.url())
expect(url.pathname).toBe('/')
expect(url.search).toBe('')
})
})
})