diff --git a/__tests__/e2e/router-race/fast.md b/__tests__/e2e/router-race/fast.md
new file mode 100644
index 000000000..a50ee5a75
--- /dev/null
+++ b/__tests__/e2e/router-race/fast.md
@@ -0,0 +1 @@
+# Fast Page
diff --git a/__tests__/e2e/router-race/index.md b/__tests__/e2e/router-race/index.md
new file mode 100644
index 000000000..0c68e3ae0
--- /dev/null
+++ b/__tests__/e2e/router-race/index.md
@@ -0,0 +1,16 @@
+
+
+# Navigation Race
+
+
+
diff --git a/__tests__/e2e/router-race/router-race.test.ts b/__tests__/e2e/router-race/router-race.test.ts
new file mode 100644
index 000000000..8a085af99
--- /dev/null
+++ b/__tests__/e2e/router-race/router-race.test.ts
@@ -0,0 +1,95 @@
+import type { Route } from 'playwright-chromium'
+
+const slowModule = /(?:\/router-race\/slow\.md\?|\/router-race_slow\.md\.)/
+
+describe.runIf(process.env.VITE_TEST_BUILD)('page load retries', () => {
+ let slowRequest: Route | undefined
+
+ beforeEach(async () => {
+ slowRequest = undefined
+ await page.route(slowModule, (route) => {
+ if (slowRequest) return route.continue()
+ slowRequest = route
+ })
+ await goto('/router-race/')
+ })
+
+ afterEach(async () => {
+ await page.unrouteAll({ behavior: 'ignoreErrors' })
+ })
+
+ async function startSlowNavigation() {
+ await page.locator('#slow').click()
+ await expect.poll(() => !!slowRequest).toBe(true)
+ }
+
+ async function navigateToFastPage() {
+ await page.locator('#fast').click()
+ await expect
+ .poll(() => page.locator('h1').textContent())
+ .toContain('Fast Page')
+ }
+
+ async function expectFastPageAfterSlowNavigation() {
+ await page.waitForFunction(
+ () => document.documentElement.dataset.slowNavigationFinished === 'true'
+ )
+ expect(await page.locator('h1').textContent()).toContain('Fast Page')
+ expect(new URL(page.url()).pathname).toBe('/router-race/fast.html')
+ }
+
+ test('ignores a failed page request after another navigation completes', async () => {
+ await startSlowNavigation()
+ await navigateToFastPage()
+ await slowRequest!.abort()
+
+ await expectFastPageAfterSlowNavigation()
+ })
+
+ test('ignores a retry after another navigation completes during a hash map fetch', async () => {
+ let hashMapRequest: Route | undefined
+ await page.route('**/hashmap.json', (route) => {
+ hashMapRequest = route
+ })
+
+ await startSlowNavigation()
+ await slowRequest!.abort()
+ await expect.poll(() => !!hashMapRequest).toBe(true)
+ await navigateToFastPage()
+ await hashMapRequest!.continue()
+
+ await expectFastPageAfterSlowNavigation()
+ })
+
+ test('retries the current page with its updated hash after a deployment', async () => {
+ await page.route('**/hashmap.json', async (route) => {
+ const response = await route.fetch()
+ const map = await response.json()
+ map['router-race_slow.md'] = 'retry'
+ await route.fulfill({ json: map })
+ })
+
+ await startSlowNavigation()
+ const response = await slowRequest!.fetch()
+ await page.route('**/router-race_slow.md.retry.js', (route) =>
+ route.fulfill({ response })
+ )
+ await slowRequest!.abort()
+
+ await page.waitForFunction(
+ () => document.documentElement.dataset.slowNavigationFinished === 'true'
+ )
+ expect(await page.locator('h1').textContent()).toContain('Slow Page')
+ expect(new URL(page.url()).pathname).toBe('/router-race/slow.html')
+ })
+
+ test('still shows the not-found page when the current retry fails', async () => {
+ await startSlowNavigation()
+ await slowRequest!.abort()
+
+ await page.waitForFunction(
+ () => document.documentElement.dataset.slowNavigationFinished === 'true'
+ )
+ expect(await page.locator('h1').textContent()).toBe('PAGE NOT FOUND')
+ })
+})
diff --git a/__tests__/e2e/router-race/slow.md b/__tests__/e2e/router-race/slow.md
new file mode 100644
index 000000000..5cb769b2e
--- /dev/null
+++ b/__tests__/e2e/router-race/slow.md
@@ -0,0 +1 @@
+# Slow Page
diff --git a/src/client/app/router.ts b/src/client/app/router.ts
index bb03338c6..6fad25582 100644
--- a/src/client/app/router.ts
+++ b/src/client/app/router.ts
@@ -106,12 +106,12 @@ export function createRouter(
if (!page) throw new Error(`Page not found: ${pendingPath}`)
if (latestPendingPath === pendingPath) {
- latestPendingPath = null
-
const { default: comp, __pageData } = page
if (!comp) throw new Error(`Invalid route component: ${comp}`)
await router.onAfterPageLoad?.(href)
+ if (latestPendingPath !== pendingPath) return
+ latestPendingPath = null
route.path = inBrowser ? pendingPath : withBase(pendingPath)
route.component = markRaw(comp)
@@ -151,10 +151,12 @@ export function createRouter(
// retry on fetch fail: the page to hash map may have been invalidated
// because a new deploy happened while the page is open. Try to fetch
// the updated pageToHash map and fetch again.
- if (!isRetry) {
+ if (!isRetry && latestPendingPath === pendingPath) {
try {
const res = await fetch(runtimeBase() + 'hashmap.json')
- ;(window as any).__VP_HASH_MAP__ = await res.json()
+ const hashMap = await res.json()
+ if (latestPendingPath !== pendingPath) return
+ ;(window as any).__VP_HASH_MAP__ = hashMap
await loadPage(href, { scrollPosition, isRetry: true, initialLoad })
return
} catch (e) {}