Repository navigation
fix: improve handling when a static asset fails to load - #3598
Conversation
With Client Routing, if fetching the page's static assets failed during hydration (e.g. flaky connection, or a new frontend was deployed while the page was loading), the server-rendered page stayed non-interactive. Vike now reloads the page in that case. To avoid an infinite reload loop, a page that was itself loaded by a reload isn't reloaded again (detected via the Navigation Timing API, so no storage is needed). Production only: in development such failures are usually caused by transform errors, which a reload doesn't fix. Fixes #3596 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013M9XEWwcvkeKwfcC9LCwgc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013M9XEWwcvkeKwfcC9LCwgc
Since the reloaded page now handles the error like any other error (logs it and renders the error page) instead of rethrowing it, the playground, which doesn't define an error page, also logs "No error page defined." — which made the test fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013M9XEWwcvkeKwfcC9LCwgc
Reloading while offline replaces the (readable) server-side rendered page with the browser's offline error page. Instead, wait for the `online` event before reloading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013M9XEWwcvkeKwfcC9LCwgc
handleErrorFetchingStaticAssets() was called before the isRenderOutdated() check, so a rendering that was already superseded (e.g. the user navigated while the error page of the first render was being loaded) could reload/redirect the page, overriding the user's navigation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013M9XEWwcvkeKwfcC9LCwgc
…ch static assets `Failed to resolve module specifier` (chromium) and `Error resolving module specifier` (firefox) are thrown for unresolvable (e.g. bare) import specifiers, before any network request is made. Reloading the page (or redirecting upon client-side navigation) can't fix them: it only added a pointless page load before the same error. They're now handled like any other error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013M9XEWwcvkeKwfcC9LCwgc
The route blocking the page's entry is set on the browser context, which is shared with the next tests of the test file, and so is the offline mode. Remove them in a `finally` block so that a failing assertion doesn't make the next tests fail too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
…n HTML-only error page If an error occurs upon hydration and the error page isn't client-side routable (e.g. an HTML-only error page), Vike used to redirect to the current URL. But the server already rendered that URL: the server renders the same page again, the error occurs again upon hydration, and so on — an infinite reload loop if the error persists. That's the case for any persistent hydration error, and also for a persistent failure to fetch static assets: once the reloaded page gives up, it handles the error like any other error. With an HTML-only error page and an entry chunk that always fails to load, the page was loaded 172 times within 8 seconds. Upon hydration, Vike now leaves the server-side rendered page as it is instead, and falls back to Server Routing: the page isn't hydrated, so Client Routing would wait forever for the hydration to end upon the next link click. The error is still logged (by handleError()). The new test/error-page-html-only/ covers both cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
…to fetch static assets When the page was already reloaded and fetching its static assets fails again, Vike gives up. It used to then handle the error like any other error, i.e. render the error page, which replaces the server-side rendered page. That's a downgrade: the server-side rendered page isn't interactive but it shows the content the user requested (and the error page itself may fail to load for the same reason). Vike now logs the error and leaves the server-side rendered page as it is. The same applies when the user reloaded the page manually. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
… once the server is reachable Reloading while the server is unreachable replaces the server-side rendered page (non-interactive, but readable) with the browser's network error page. Checking `navigator.onLine` isn't enough: it's `true` whenever the device is connected to a network, regardless of whether the server is reachable (e.g. upon a brief loss of mobile connection). Vike now first checks whether the server is reachable (a HEAD request to the page's URL) and reloads only once it is. Otherwise it retries after 1s, 2s, 4s, ... (up to every 60s), and right away when the browser goes back online. Any HTTP response counts as reachable, except server errors (5xx). Vike doesn't use status codes to tell apart a new frontend deployment from a flaky connection: the old static assets aren't always `404` (e.g. S3 replies `403`, and static hosts with SPA fallback reply `200` with HTML) and the failed dynamic import gives the same error in all cases — while a reload fixes both anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
Form fields work without hydration: the user may have started filling a form of the server-side rendered page while it was loading (or while Vike was waiting for the server to be reachable again). Reloading the page would discard what the user entered. In that case, Vike now logs the error and leaves the server-side rendered page as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
This reverts commit 70c519e. Its value is limited: without hydration, the form's client-side submit handler doesn't run, so the user can't submit what they typed anyway — keeping the page only lets them copy it, while the page stays non-interactive. Not worth its client-side KBs (~120 bytes gzipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
…r with an HTML-only error page" This reverts commit f9cb8db. Moved to a separate PR: since "keep the server-side rendered page when the reloaded page fails to fetch static assets", failing to fetch static assets no longer reaches renderErrorPage() upon hydration, so the loop fixed by that commit is unrelated to this PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
Upon failing to fetch static assets upon hydration, reload the page only if some of the page's static assets don't exist anymore, i.e. a new frontend was deployed. Upon a flaky connection, let it fail as before: the server-side rendered page remains and, since Client Routing is disabled, the next page navigation is a full page load. The error thrown by `import()` is the same for a missing asset and for a network error, so the page's `modulepreload` assets are checked with HEAD requests: a non-2xx response or a response that isn't JavaScript (e.g. S3's `403`, or the `200` HTML page of an SPA fallback) means a new deployment, while a network error means a flaky connection. This replaces waiting for the server to be reachable before reloading (retry timer, `online` listener, server probe). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
The first modulepreload link is the page's entry, and the hash in its file name changes whenever any of the page's JavaScript changes (the hash of a file includes the hashes of the files it imports). Checking it alone is therefore enough: one HEAD request instead of one per modulepreload link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
Instead of checking the response's status and content type, only check whether the server responds at all. If it does, then either a new frontend was deployed or the connection is back: either way, reloading fixes the page. Upon a network error, we let it fail as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
This reverts commit fa75b50.
|
Review my last two commits. |
showErrorPage() called disableClientRouting(), which asserts that the error is a failure to fetch static assets. Any other error, such as the page's code throwing while it's loaded on the client-side, or any error in development, thus threw Vike's "You stumbled upon a Vike bug" assertion instead of rendering the error page. Now only the path that handles a failure to fetch static assets (a blank page that was already reloaded) disables Client Routing. New test: test/abort's /load-error page throws while it's loaded on the client-side, and Vike renders the error page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
- Vike now logs a single message upon failing to fetch static assets. - Vike now checks the connection with a HEAD request to the page's URL instead of the page's entry, which the test doesn't block. So the SPA test now asserts that Vike doesn't check the connection at all: without the blank page rule, the test fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
|
Reviewed 4f962c3 and c430ba9. There's one bug and a broken test, both fixed in 13591a8 and 46bb81f. The rest looks good. Bug (4f962c3): Fixed in 13591a8: No test covered this, so I added one to Tests: CI is red on c430ba9 because "the page is reloaded only once" expects the log message that 4f962c3 removed. 46bb81f updates the expected message in both tests that check it. Also, c430ba9 checks the connection on the page's URL, which the tests don't block. The SPA test would therefore keep passing without the blank page rule. It now asserts that Vike doesn't check the connection at all, and I checked that it fails without the rule. c430ba9 (
4f962c3 (three actions): Reads much better. Together, the two commits make the client code 99 B smaller minified (63 B gzipped). Showing the error page for a reloaded blank page is a good fallback. Its own code may fail to load for the same reason, but then the page just stays blank: Nits I didn't change:
Generated by Claude Code |
|
97898cc => good? |
|
With 7cc7abf, yes 👍 97898cc on its own wasn't: I had made the same change locally. With it, Moving Generated by Claude Code |
|
Review my last commits |
|
f3410f6, 9c7b543 and 512955d are all good 👍 They fix the three nits, and none of them changes behavior. The tests only match the beginning of the log message, so the "Server Routing" casing doesn't affect them. One optional nit: Generated by Claude Code |
|
Then do one last round of review |
|
Last round, on the whole diff against I traced every path:
Optional suggestions, your call:
Tiny nit: "counter productive" → "counterproductive". CI on 512955d is still running. Generated by Claude Code |
Fix the typo |
On a flaky connection, the HEAD request may hang and succeed much later, which reloaded the page long after it was shown (e.g. while the user is filling a form). After 5 seconds, Vike now considers the connection down and lets it fail. In browsers without AbortSignal.timeout(), the call throws inside the `try`, so Vike doesn't reload. New test: the connection check never gets a response, and the page isn't reloaded. Also fix a typo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD
|
Done in 63ed311:
Generated by Claude Code |
|
27d7e89 (4 s timeout) 👍 4f4df97 removes all of this PR's tests: the six playground hydration retry tests and If run time is the concern, I can restore a smaller set: If the tests stay removed, the pages they used are now unused: Generated by Claude Code |
Closes #3596
Builds on #3597: this branch includes its 12 commits, plus the commits listed below.
What changes
With Client Routing, the server-rendered page stays non-interactive when fetching the page's static assets fails during hydration. The failed
import()throws the same error whether the asset is gone (a new frontend was deployed) or the connection failed. So Vike now checks the connection with aHEADrequest to the page's URL:The server responds → reload once. Most likely a new frontend was deployed, or else the connection is back. Either way, reloading fixes the page.
Network error, or no response within 4 seconds → let it fail. Vike behaves as on
main:Recovering from a flaky connection is left to the browser and the user. Reloading while the connection is down would replace the server-rendered page with the browser's offline page. The timeout prevents a reload long after the page was shown, e.g. while the user is filling a form.
Blank page (e.g. SPA) → reload once, without checking. A reload is better than a blank page, even if the connection is down: at worst the browser shows its offline page. Vike doesn't know whether the page was server-side rendered (
ssr: falseis a vike-react/vue/solid setting), so it checks whether the page shows any text (document.body.innerText).Reload only once. Vike uses the Navigation Timing API to detect a page that was itself loaded by a reload, and doesn't reload it again. This covers e.g. a cached HTML page that keeps referencing the old assets. If such a page is blank, Vike renders its error page instead.
Production only. In development, such failures are usually transform errors, which a reload doesn't fix. They're now handled like any other error.
Upon client-side navigation, Vike still does a full page load of the target URL. The exception is an outdated navigation, i.e. the user navigated again in the meantime. Separately, module specifier resolution errors are no longer treated as failures to fetch static assets.
Vike logs a single message upon failing to fetch static assets: "Failed to fetch static asset (due to a connection issue, or a new deployment) — falling back to Server Routing".
Client-side cost: +268 B minified / +117 B gzipped compared to
main. Measured onrenderPageClient.tsandloadPageConfigsLazyClientSide.tswith esbuild minify + gzip.Commits on top of #3597
test:the hydration retry tests always remove their routes and offline mode, even when an assertion failspolish:reword the log upon failing to fetch static assetsfeat:upon hydration, reload only if a new frontend was deployedfeat: … reload only once the server is reachable.main. This replaces the earlierfix: keep the server-side rendered page …, which logged the error instead.minor:check only the page's entryminor:reload unless the connection is down, instead of checking the response's status and content typeminor:say so when the connection is downminor:renameisServerReachable()toisLikelyNewDeployment()and explain return valuesminor:name the return values ofhandleErrorFetchingStaticAssets()minor:renameisLikelyNewDeployment()toisNetworkError()feat:upon hydration, reload a blank page (e.g. SPA) even upon a network errorminor:simplify the log upon failing to fetch static assetshandleErrorFetchingStaticAssets()is split into three actions: show the error page, keep the broken server-rendered page, or hard reload. A reloaded blank page shows the error page. There's a single log message.injectFilter()edge case.fix:render the error page if the page's code throws while it's loaded.showErrorPage()asserted that every error was a failure to fetch static assets.test:update the hydration retry tests to the single log message and the page URL checkminor:give up the connection check after a timeoutReverted:
feat: don't reload the page if the user entered data: without hydration the form can't be submitted anyway, so it isn't worth its client-side KBs.fix: no infinite reload loop upon a persistent hydration error with an HTML-only error page: static asset failures no longer reach that code path. Also, fix: always use client router to render the error page #3600 has since removed the error page check that caused the loop.Tests
None: 4f4df97 and 1758b98 removed the tests this PR had added, along with their pages. The PR now only changes
renderPageClient.tsandloadPageConfigsLazyClientSide.ts.🤖 Generated with Claude Code
https://claude.ai/code/session_01VdMeoWMUuZtWBoUosW7ZqD