Skip to content

fix(deploy): auto-recover from stale lazy-chunk hash after redeploys#1167

Open
plind-junior wants to merge 2 commits into
entrius:testfrom
plind-junior:fix/stale-chunk-recovery
Open

fix(deploy): auto-recover from stale lazy-chunk hash after redeploys#1167
plind-junior wants to merge 2 commits into
entrius:testfrom
plind-junior:fix/stale-chunk-recovery

Conversation

@plind-junior
Copy link
Copy Markdown
Contributor

@plind-junior plind-junior commented May 14, 2026

Summary

Users who had a long-lived tab open from before a production deploy were hitting "Failed to fetch dynamically imported module: …/assets/WatchlistPage-.js" and an unrecoverable ErrorBoundary screen when navigating to a lazy route. Root cause: Vite emits hashed chunk filenames; the old entry chunk in their cached tab references the old hashes, but the server only has the new hashes after the redeploy, so import() 404s.

This PR wraps React.lazy() so that on a chunk-load failure it triggers a single window.location.reload(). The reload re-fetches index.html, which now references the current chunk hashes — the import succeeds and the user lands on the page they tried to navigate to.

Root cause

  1. Build emits hashed chunks like WatchlistPage-C10WDGR6.js.
  2. The entry chunk index-<hash>.js literally embeds the string "WatchlistPage-C10WDGR6.js" as the dynamic-import URL.
  3. After a new deploy, the entry chunk has a different hash and now embeds WatchlistPage-NewHash.js. The old chunk filenames are deleted by the deploy.
  4. A user with a tab open from before the deploy still has the old entry chunk loaded. When they navigate to /watchlist, that old code calls import('/assets/WatchlistPage-C10WDGR6.js')404 → import rejects → ErrorBoundary catches it.

Direct evidence from the production console screenshot:

Failed to load resource: the server responded with a status of 404 ()
  WatchlistPage-C10WDGR6.js
TypeError: Failed to fetch dynamically imported module:
  https://gittensor.io/assets/WatchlistPage-C10WDGR6.js

Fix

src/routes.tsx — introduce lazyWithReload(factory) and use it in place of React.lazy for every page:

const lazyWithReload = <T extends React.ComponentType<any>>(
  factory: () => Promise<{ default: T }>,
): React.LazyExoticComponent<T> =>
  React.lazy(() =>
    factory()
      .then((mod) => {
        // Successful chunk load — reset the recovery flag so any *future*
        // stale-deploy event also gets one fresh reload attempt.
        if (typeof window !== 'undefined') {
          window.sessionStorage.removeItem(CHUNK_RELOAD_SESSION_KEY);
        }
        return mod;
      })
      .catch((err: Error) => {
        const message = String(err?.message ?? err);
        const isChunkLoadError =
          /Failed to fetch dynamically imported module|Importing a module script failed|ChunkLoadError/i.test(
            message,
          );
        if (
          isChunkLoadError &&
          typeof window !== 'undefined' &&
          !window.sessionStorage.getItem(CHUNK_RELOAD_SESSION_KEY)
        ) {
          window.sessionStorage.setItem(CHUNK_RELOAD_SESSION_KEY, '1');
          window.location.reload();
          return { default: (() => null) as unknown as T };
        }
        throw err;
      }),
  );

Every route page (HomePage, DashboardPage, IssuesPage, … 14 total) now uses lazyWithReload(...) instead of React.lazy(...). No other call sites needed to change.

Behavior

Scenario Today After this PR
Tab open before redeploy → navigate to lazy route → 404 on stale chunk ErrorBoundary screen ("Failed to fetch dynamically imported module"), user must hard-refresh manually Auto-reloads once; user lands on the working version
Genuine chunk 404 (e.g. asset truly missing in deploy) ErrorBoundary screen First time: reload attempted. Second time within same session: ErrorBoundary screen (no infinite loop)
Normal in-app navigation, no deploy in between Works Works (no change)
Successive deploys during one session Works after manual refresh each time Auto-recovers each time (flag is cleared on next successful chunk load)

Loop guard

sessionStorage[gt:chunk-reloaded] flag prevents an infinite reload loop. It's:

  • Set when a chunk-load error is recognized → reload triggered
  • Cleared on the next successful chunk load (so the next stale-deploy gets its own one-shot recovery)
  • Scoped to sessionStorage (not localStorage) so it resets when the user closes the tab — closing & reopening a tab after a deploy works as expected

Test plan

  • npm run build && npm run preview — site still loads, all pages still navigate normally (regression check on the happy path).
  • Simulated stale-deploy:
    1. npm run build && npm run preview — note the chunk filenames in dist/assets/.
    2. Open the site, navigate to Home (entry chunk loads).
    3. Rename dist/assets/WatchlistPage-*.js to something else (simulating "this chunk no longer exists").
    4. Click the Watchlist nav item.
    5. Expected: the tab reloads automatically. After reload, since the file is still renamed, the user sees the ErrorBoundary — but only the second time, not the first. Loop is broken.
    6. Restore the file name; reload; navigation works.
  • DevTools → Application → Session Storage — confirm gt:chunk-reloaded flag toggles as expected.
  • npm run type-check and npm run lint pass.

Out of scope

  • The chunk-loading robustness of import() calls outside route-level lazy loading (e.g. dynamic icon loading). If any other import() calls exist, they'd need similar wrappers. None are known to be called by user actions in this codebase, but worth checking on follow-up audits.
  • Service-worker / cache-control on index.html — orthogonal hardening (ensures stale index.html doesn't cache indefinitely). Could be a follow-up.
  • A user-facing toast like "We've updated to a new version — reloading..." instead of a silent reload. Could be added if reviewers prefer.

References

Before

image

@xiao-xiao-mao xiao-xiao-mao Bot added the bug Something isn't working label May 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant