diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index 2ce372fc03d85..9fa595a67650d 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -93,16 +93,23 @@ function doMpaNavigation(url: string): FetchServerResponseResult { } } -// TODO: Figure out why this module is included in server page bundles. -const win = typeof window === 'undefined' ? undefined : window -const abortController = new AbortController() +let abortController = new AbortController() + +if (typeof window !== 'undefined') { + // Abort any in-flight requests when the page is unloaded, e.g. due to + // reloading the page or performing hard navigations. This allows us to ignore + // what would otherwise be a thrown TypeError when the browser cancels the + // requests. + window.addEventListener('pagehide', () => { + abortController.abort() + }) -// Abort any in-flight requests when the page is unloaded, e.g. due to reloading -// the page or performing hard navigations. This allows us to ignore what would -// otherwise be a thrown TypeError when the browser cancels the requests. -win?.addEventListener('pagehide', () => { - abortController.abort() -}) + // Use a fresh AbortController instance on pageshow, e.g. when navigating back + // and the JavaScript execution context is restored by the browser. + window.addEventListener('pageshow', () => { + abortController = new AbortController() + }) +} /** * Fetch the flight data for the provided url. Takes in the current router state @@ -152,7 +159,12 @@ export async function fetchServerResponse( : 'low' : 'auto' - const res = await createFetch(url, headers, fetchPriority) + const res = await createFetch( + url, + headers, + fetchPriority, + abortController.signal + ) const responseUrl = urlToUrlWithoutFlightMarker(res.url) const canonicalUrl = res.redirected ? responseUrl : undefined diff --git a/test/e2e/app-dir/app-prefetch/app/page.js b/test/e2e/app-dir/app-prefetch/app/page.js index 1f152de4f44cc..8d9166b1ec0e6 100644 --- a/test/e2e/app-dir/app-prefetch/app/page.js +++ b/test/e2e/app-dir/app-prefetch/app/page.js @@ -14,6 +14,9 @@ export default function HomePage() { To Dynamic Slug Page + + Hard Nav to Static Page + ) } diff --git a/test/e2e/app-dir/app-prefetch/app/static-page/back-button.js b/test/e2e/app-dir/app-prefetch/app/static-page/back-button.js new file mode 100644 index 0000000000000..e29215399d8f9 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/static-page/back-button.js @@ -0,0 +1,15 @@ +'use client' + +export function BackButton() { + return ( + + ) +} diff --git a/test/e2e/app-dir/app-prefetch/app/static-page/page.js b/test/e2e/app-dir/app-prefetch/app/static-page/page.js index 73fda2a3aa729..006547ee01bef 100644 --- a/test/e2e/app-dir/app-prefetch/app/static-page/page.js +++ b/test/e2e/app-dir/app-prefetch/app/static-page/page.js @@ -1,4 +1,5 @@ import Link from 'next/link' +import { BackButton } from './back-button' export default async function Page() { return ( @@ -14,6 +15,9 @@ export default async function Page() { To Same Page

+

+ +

) } diff --git a/test/e2e/app-dir/app-prefetch/prefetching.test.ts b/test/e2e/app-dir/app-prefetch/prefetching.test.ts index 836e84efb287f..cbb6765a3559e 100644 --- a/test/e2e/app-dir/app-prefetch/prefetching.test.ts +++ b/test/e2e/app-dir/app-prefetch/prefetching.test.ts @@ -79,7 +79,7 @@ describe('app dir - prefetching', () => { it('should not have prefetch error when reloading before prefetch request is finished', async () => { const browser = await next.browser('/') - await browser.eval('window.nd.router.prefetch("/dashboard/123")') + await browser.eval('window.next.router.prefetch("/dashboard/123")') await browser.refresh() const logs = await browser.log() @@ -92,6 +92,32 @@ describe('app dir - prefetching', () => { ) }) + it('should not suppress prefetches after navigating back', async () => { + if (!process.env.CI && process.env.HEADLESS) { + console.warn('This test can only run in headed mode. Skipping...') + return + } + + // Force headed mode, as bfcache is not available in headless mode. + const browser = await next.browser('/', { headless: false }) + + // Trigger a hard navigation. + await browser.elementById('to-static-page-hard').click() + + // Go back, utilizing the bfcache. + await browser.elementById('go-back').click() + + let requests: string[] = [] + browser.on('request', (req) => { + requests.push(new URL(req.url()).pathname) + }) + + await browser.evalAsync('window.next.router.prefetch("/dashboard/123")') + await browser.waitForIdleNetwork() + + expect(requests).toInclude('/dashboard/123') + }) + it('should not fetch again when a static page was prefetched', async () => { const browser = await next.browser('/404', browserConfigWithFixedTime) let requests: string[] = []