Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,20 @@ function doMpaNavigation(url: string): FetchServerResponseResult {
return urlToUrlWithoutFlightMarker(new URL(url, location.origin)).toString()
}

let abortController = new AbortController()
let isPageUnloading = false

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.
// Track when the page is unloading, e.g. due to reloading the page or
// performing hard navigations. This allows us to suppress error logging when
// the browser cancels in-flight requests during page unload.
window.addEventListener('pagehide', () => {
abortController.abort()
isPageUnloading = true
})

// Use a fresh AbortController instance on pageshow, e.g. when navigating back
// and the JavaScript execution context is restored by the browser.
// Reset the flag on pageshow, e.g. when navigating back and the JavaScript
// execution context is restored by the browser.
window.addEventListener('pageshow', () => {
abortController = new AbortController()
isPageUnloading = false
})
}

Expand Down Expand Up @@ -197,8 +196,7 @@ export async function fetchServerResponse(
url,
headers,
fetchPriority,
shouldImmediatelyDecode,
abortController.signal
shouldImmediatelyDecode
)

const responseUrl = urlToUrlWithoutFlightMarker(new URL(res.url))
Expand Down Expand Up @@ -287,7 +285,7 @@ export async function fetchServerResponse(
debugInfo: flightResponsePromise._debugInfo ?? null,
}
} catch (err) {
if (!abortController.signal.aborted) {
if (!isPageUnloading) {
console.error(
`Failed to fetch RSC payload for ${originalUrl}. Falling back to browser navigation.`,
err
Expand Down
13 changes: 13 additions & 0 deletions test/e2e/app-dir/fetch-abort-on-refresh/app/(root1)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>{children}</body>
</html>
)
}
19 changes: 19 additions & 0 deletions test/e2e/app-dir/fetch-abort-on-refresh/app/(root1)/mpa.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client'

import { useRouter } from 'next/navigation'

export function TriggerMpaNavigation() {
const router = useRouter()
return (
<button
id="trigger-navigation"
onClick={async () => {
router.push('/slow-page')
await new Promise((resolve) => setTimeout(resolve, 500))
router.push('/other-root')
}}
>
Trigger Navigation
</button>
)
}
10 changes: 10 additions & 0 deletions test/e2e/app-dir/fetch-abort-on-refresh/app/(root1)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { TriggerMpaNavigation } from './mpa'

export default function StartPage() {
return (
<div style={{ padding: '20px' }}>
<h1 id="start-page">Start Page</h1>
<TriggerMpaNavigation />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { connection } from 'next/server'
import { Suspense } from 'react'

async function SlowData() {
// Artificial delay to simulate slow RSC fetch
await connection()
await new Promise((resolve) => setTimeout(resolve, 5_000))
return (
<div
id="slow-data-loaded"
style={{ padding: '10px', background: '#e0e0ff' }}
>
Slow data loaded successfully!
</div>
)
}

export default function SlowPage() {
return (
<div id="slow-page">
<Suspense
fallback={<div id="loading-slow-data">Loading slow data...</div>}
>
<SlowData />
</Suspense>
</div>
)
}
13 changes: 13 additions & 0 deletions test/e2e/app-dir/fetch-abort-on-refresh/app/(root2)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>{children}</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function StartPage() {
return <div id="root-2">Root 2</div>
}
19 changes: 19 additions & 0 deletions test/e2e/app-dir/fetch-abort-on-refresh/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client' // Error boundaries must be Client Components

export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
// global-error must include html and body tags
<html>
<body>
<h2 id="global-error">Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { nextTestSetup } from 'e2e-utils'

const describeHeaded = process.env.HEADLESS ? describe.skip : describe

describeHeaded('fetch-abort-on-refresh', () => {
const { next } = nextTestSetup({
files: __dirname,
})

it('should not show abort error in global error boundary when restoring from bfcache', async () => {
// This test ensures that when restoring a page from the browser bfcache that was pending RSC data,
// that the abort does not propagate to a user's error boundary.
const browser = await next.browser('/', { headless: false })

await browser.elementById('trigger-navigation').click()

await browser.waitForElementByCss('#root-2')

// Go back to trigger bfcache restoration
// we overwrite the typical waitUntil: 'load' option here as the event is never being triggered if we hit the bfcache
await browser.back({ waitUntil: 'commit' })

// Check that we're back on the slow page (the page that was first redirected to, before the MPA, not the error boundary)
// We use element checks instead of eval() because eval() triggers waitForLoadState which times out with bfcache
const hasSlowPage = await browser.hasElementByCss('#slow-page')
const hasGlobalError = await browser.hasElementByCss(
'h2:has-text("Something went wrong!")'
)

expect(hasSlowPage).toBe(true)
expect(hasGlobalError).toBe(false)
})
})
Loading