Skip to content

Commit e08fe27

Browse files
authored
Restore RSC fetch error handling after navigating back (#73985)
As a follow-up to #73975, we're now restoring the error handling for failed RSC fetch calls when the user navigates back after a hard navigation. In this case the browser uses the bfcache to restore the previous JavaScript execution context, and thus the abort controller will still be in the aborted state. To take this into account, we're now creating a new `AbortController` instance on `'pageshow'` events. In addition, the abort controller's `signal` is now actually passed to the `fetch` call, and not only used for the error handling, so that the requests are aborted on `'pagehide'`. This was an oversight in the original PR. With that, it's even more important to create a fresh abort controller, otherwise RSC fetching would be disabled after back navigation. The added e2e test can only run in headed mode unfortunately, as the bfcache is not available in headless mode. (Using the same approach as in #54081.)
1 parent e9415fc commit e08fe27

File tree

5 files changed

+71
-11
lines changed

5 files changed

+71
-11
lines changed

packages/next/src/client/components/router-reducer/fetch-server-response.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,23 @@ function doMpaNavigation(url: string): FetchServerResponseResult {
9393
}
9494
}
9595

96-
// TODO: Figure out why this module is included in server page bundles.
97-
const win = typeof window === 'undefined' ? undefined : window
98-
const abortController = new AbortController()
96+
let abortController = new AbortController()
97+
98+
if (typeof window !== 'undefined') {
99+
// Abort any in-flight requests when the page is unloaded, e.g. due to
100+
// reloading the page or performing hard navigations. This allows us to ignore
101+
// what would otherwise be a thrown TypeError when the browser cancels the
102+
// requests.
103+
window.addEventListener('pagehide', () => {
104+
abortController.abort()
105+
})
99106

100-
// Abort any in-flight requests when the page is unloaded, e.g. due to reloading
101-
// the page or performing hard navigations. This allows us to ignore what would
102-
// otherwise be a thrown TypeError when the browser cancels the requests.
103-
win?.addEventListener('pagehide', () => {
104-
abortController.abort()
105-
})
107+
// Use a fresh AbortController instance on pageshow, e.g. when navigating back
108+
// and the JavaScript execution context is restored by the browser.
109+
window.addEventListener('pageshow', () => {
110+
abortController = new AbortController()
111+
})
112+
}
106113

107114
/**
108115
* Fetch the flight data for the provided url. Takes in the current router state
@@ -152,7 +159,12 @@ export async function fetchServerResponse(
152159
: 'low'
153160
: 'auto'
154161

155-
const res = await createFetch(url, headers, fetchPriority)
162+
const res = await createFetch(
163+
url,
164+
headers,
165+
fetchPriority,
166+
abortController.signal
167+
)
156168

157169
const responseUrl = urlToUrlWithoutFlightMarker(res.url)
158170
const canonicalUrl = res.redirected ? responseUrl : undefined

test/e2e/app-dir/app-prefetch/app/page.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export default function HomePage() {
1414
<Link href="/prefetch-auto/foobar" id="to-dynamic-page">
1515
To Dynamic Slug Page
1616
</Link>
17+
<a href="/static-page" id="to-static-page-hard">
18+
Hard Nav to Static Page
19+
</a>
1720
</>
1821
)
1922
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use client'
2+
3+
export function BackButton() {
4+
return (
5+
<button
6+
type="button"
7+
id="go-back"
8+
onClick={() => {
9+
window.history.back()
10+
}}
11+
>
12+
Go back
13+
</button>
14+
)
15+
}

test/e2e/app-dir/app-prefetch/app/static-page/page.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Link from 'next/link'
2+
import { BackButton } from './back-button'
23

34
export default async function Page() {
45
return (
@@ -14,6 +15,9 @@ export default async function Page() {
1415
To Same Page
1516
</Link>
1617
</p>
18+
<p>
19+
<BackButton />
20+
</p>
1721
</>
1822
)
1923
}

test/e2e/app-dir/app-prefetch/prefetching.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe('app dir - prefetching', () => {
7979

8080
it('should not have prefetch error when reloading before prefetch request is finished', async () => {
8181
const browser = await next.browser('/')
82-
await browser.eval('window.nd.router.prefetch("/dashboard/123")')
82+
await browser.eval('window.next.router.prefetch("/dashboard/123")')
8383
await browser.refresh()
8484
const logs = await browser.log()
8585

@@ -92,6 +92,32 @@ describe('app dir - prefetching', () => {
9292
)
9393
})
9494

95+
it('should not suppress prefetches after navigating back', async () => {
96+
if (!process.env.CI && process.env.HEADLESS) {
97+
console.warn('This test can only run in headed mode. Skipping...')
98+
return
99+
}
100+
101+
// Force headed mode, as bfcache is not available in headless mode.
102+
const browser = await next.browser('/', { headless: false })
103+
104+
// Trigger a hard navigation.
105+
await browser.elementById('to-static-page-hard').click()
106+
107+
// Go back, utilizing the bfcache.
108+
await browser.elementById('go-back').click()
109+
110+
let requests: string[] = []
111+
browser.on('request', (req) => {
112+
requests.push(new URL(req.url()).pathname)
113+
})
114+
115+
await browser.evalAsync('window.next.router.prefetch("/dashboard/123")')
116+
await browser.waitForIdleNetwork()
117+
118+
expect(requests).toInclude('/dashboard/123')
119+
})
120+
95121
it('should not fetch again when a static page was prefetched', async () => {
96122
const browser = await next.browser('/404', browserConfigWithFixedTime)
97123
let requests: string[] = []

0 commit comments

Comments
 (0)