From d76137e9a3cd7175d97c51eb4d639b11ec13b8e3 Mon Sep 17 00:00:00 2001 From: Sean Kirby Date: Mon, 2 Mar 2026 23:35:18 -0500 Subject: [PATCH] Fix unhandled promise rejection when prefetch request fails PrefetchCache.putLater calls request.perform() without catching errors. FetchRequest.perform() re-throws non-AbortErrors after delegating. In Safari, navigating away from a page while a prefetch is in-flight causes the browser to cancel the fetch with a TypeError instead of an AbortError. Since only AbortErrors are silently handled by FetchRequest, this surfaces as an unhandled rejection in Safari/iOS: TypeError: Unhandled Promise Rejection: Load failed Catch the rejection and evict the stale cache entry so that a fresh request is made when the user clicks the link. --- src/core/drive/prefetch_cache.js | 2 +- .../link_prefetch_observer_tests.js | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/core/drive/prefetch_cache.js b/src/core/drive/prefetch_cache.js index cb29aa238..bb9cec0ca 100644 --- a/src/core/drive/prefetch_cache.js +++ b/src/core/drive/prefetch_cache.js @@ -14,7 +14,7 @@ class PrefetchCache extends LRUCache { putLater(url, request, ttl) { this.#prefetchTimeout = setTimeout(() => { - request.perform() + request.perform().catch(() => this.evict(toCacheKey(url))) this.put(url, request, ttl) this.#prefetchTimeout = null }, this.prefetchDelay) diff --git a/src/tests/functional/link_prefetch_observer_tests.js b/src/tests/functional/link_prefetch_observer_tests.js index a40ea16dc..9ae795d9d 100644 --- a/src/tests/functional/link_prefetch_observer_tests.js +++ b/src/tests/functional/link_prefetch_observer_tests.js @@ -205,6 +205,25 @@ test("doesn't include a turbo-frame header when the link is inside a turbo frame }}) }) +test("it doesn't cause an unhandled rejection when a prefetch request fails", async ({ page }) => { + await goTo({ page, path: "/hover_to_prefetch.html" }) + + await page.evaluate(() => { + window.__unhandledRejections = [] + window.addEventListener("unhandledrejection", (event) => { + window.__unhandledRejections.push(event.reason?.message ?? String(event.reason)) + }) + }) + + await page.route("**/prefetched.html", (route) => route.abort("failed")) + + await page.hover("#anchor_for_prefetch") + await sleep(200) + + const unhandledRejections = await page.evaluate(() => window.__unhandledRejections) + expect(unhandledRejections).toHaveLength(0) +}) + test("it prefetches links with a delay", async ({ page }) => { await goTo({ page, path: "/hover_to_prefetch.html" })