Skip to content

fix: reload page once if hydration fails to fetch static assets - #3597

Closed
brillout-agent wants to merge 12 commits into
vikejs:mainfrom
brillout-agent:claude/tender-goldberg-6yx3cq
Closed

brillout-agent wants to merge 12 commits into
vikejs:mainfrom
brillout-agent:claude/tender-goldberg-6yx3cq

Conversation

@brillout-agent

Copy link
Copy Markdown

Closes #3596

With Client Routing, when fetching the page's static assets fails during hydration (flaky connection, or a new frontend deployed while the page was loading), the server-rendered page stays non-interactive and the error surfaces as an unhandled rejection.

Vike now reloads the page in that case, but only once: a page that was itself loaded by a reload isn't reloaded again, so persistent failures can't cause a reload loop.

  • The "already reloaded" check uses the Navigation Timing API (performance.getEntriesByType('navigation')[0].type === 'reload'), so no sessionStorage/localStorage flag is needed. If the type can't be determined (older browsers), Vike doesn't reload.
  • If the reloaded page fails again, Vike rethrows the original error unchanged, as before.
  • Production only: in development such failures are usually transform errors, which a reload doesn't fix.
  • Trade-off: if hydration fails after the user manually reloaded the page, there's no automatic retry.

Tests (test/playground, production builds only):

  • Entry chunk blocked on the first load: the page reloads once and becomes interactive. This test fails without the fix.
  • Entry chunk blocked on every load: exactly two page loads, then the error is thrown.

Docs: new section on /deploy-sync, and an entry for it in llms.txt.

Side note: with the page proxy of @brillout/test-e2e, page.unroute() is a silent no-op (Playwright assigns this._routes, which lands on the proxy target), so the new tests route via page.context(). The existing page.unroute() call in the streamed-values test is affected too.

🤖 Generated with Claude Code

https://claude.ai/code/session_013M9XEWwcvkeKwfcC9LCwgc

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 vikejs#3596

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013M9XEWwcvkeKwfcC9LCwgc
@brillout

Copy link
Copy Markdown
Member

Is the fix accurate? Do we need to make the distinction whether it's a failure upon first page load or upon client-side navigation?

@brillout-agent

Copy link
Copy Markdown
Author

Is the fix accurate?

Yes. I reproduced the issue in test/playground (production build): when the page's entry chunk fails to load during hydration, the page stays non-interactive and the error surfaces as an unhandled rejection. With this PR, the page reloads once and becomes interactive. If the chunk keeps failing, there are exactly two page loads and then the original error is rethrown unchanged, so there's no reload loop.

Retrying the import() without reloading wouldn't work. In Chromium, the entry chunk is requested only once per page load (by the <link rel="modulepreload"> that Vike injects), and the later import() fails right away without a new request, because the browser caches the failed fetch. A reload starts from scratch, and in the new-deployment case it also gets fresh HTML.

Do we need to make the distinction whether it's a failure upon first page load or upon client-side navigation?

Yes. The recovery differs, and the code already distinguished the two cases before this PR (isFirstRender in handleErrorFetchingStaticAssets()):

  • Client-side navigation (unchanged): Vike falls back to a full page load of the target URL (disableClientRouting() + redirectHard()). That page load is itself the retry, since it fetches fresh HTML and assets. It doesn't need a loop guard: it only happens upon a user navigation, and if the target page then fails to hydrate, the first-page-load logic below applies.
  • First page load (what this PR changes): there's no other page to go to, so the only recovery is reloading the current page. That's the case that needs a loop guard, and it's why it uses location.reload() rather than redirectHard(): the guard checks whether the current page was loaded by a reload, and location.href = url counts as a regular navigation.

The two paths combine without looping. I checked this in the browser: a client-side navigation whose target always fails leads to a full page load, then one reload, then Vike gives up (two page loads in total).

Known limits:

  • production only
  • no automatic retry if the user had reloaded the page manually
  • the Server Routing runtime still has no recovery; I left it out of this PR, it could be a follow-up

Generated by Claude Code

brillout and others added 10 commits October 11, 2026 10:57
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
@brillout

Copy link
Copy Markdown
Member

@brillout brillout closed this Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Retry hydration upon flaky connection

3 participants