Skip to content

An app whose shell no longer matches the server repairs itself - #130

Merged
suskozaver merged 1 commit into
ArunNGun:betafrom
suskozaver:fix/a-broken-shell-repairs-itself
Sep 17, 2026
Merged

suskozaver merged 1 commit into
ArunNGun:betafrom
suskozaver:fix/a-broken-shell-repairs-itself

Conversation

@suskozaver

Copy link
Copy Markdown
Collaborator

Reported after a deploy: a white screen, "Application error: a client-side exception has occurred", and ChunkLoadError: Loading chunk 135 failed in the console.

What the hashes said

file the browser the new build
255-… 3d881dfa8c72bc56 3d881dfa8c72bc56
135-… d5de4c531d4e06a7 b4d0965ad68280be
webpack-… 55676dfde347fa72 9100694e6ef91c55

The runtime was the old one, out of the service worker's cache. It asked for chunk 135 by its old name and the new build calls that file something else, so the request 404ed. Chunk 255 loaded perfectly, because its contents had not changed between the builds and it therefore kept its name. Half old and half new, which is the exact thing the per-build cache exists to prevent.

It does prevent it, for files it holds. What it cannot prevent is a chunk it never held: the cache fills by visiting, chunks are lazy, and a deploy takes the old ones off the server. From that moment, any chunk the old shell had not already cached is gone for good.

And the way out is the update prompt, which lives inside the app that cannot boot.

The fix

A way out that does not live there. An inline script in the head, ahead of the app's own code, listening for that failure and only that one: drop the caches, unregister the worker, reload once.

Explicit updates stay, because they are a good decision and this does not touch them. The script speaks up only when the shell is already broken and there is nothing left to protect.

Twice in ten minutes and it stops. If clearing the cache did not help then the fault is on the server, and a page that reloads forever is one broken page turned into a machine hammering it.

Two things about the shape

Inline rather than a component, because by the time React could mount a component, the chunk it needs may be the missing one.

Serialised from the real functions with toString() rather than the same rules typed out again as a string. Two copies of a rule is how one of them gets fixed and the other does not, and there is a test holding the script to the functions.

Checks

npx tsc --noEmit, npx next lint, npm test and TZ=America/New_York npm test: 65 files, 1684 tests, no warnings.

Eighteen of them are new. Five run the serialised source in a node:vm sandbox with the browser stubbed out and assert what it does to the caches, the registration and the location, because source that has never been run has never been checked. The built output was also read: the listener is in the HTML of every prerendered page.

Reported after a deploy: a white screen, "Application error: a
client-side exception", and ChunkLoadError: Loading chunk 135 failed.

The hashes said what happened. The browser was running
webpack-55676dfde347fa72.js where the new build has
webpack-9100694e6ef91c55.js, so the runtime came out of the service
worker's cache. It asked for 135-d5de4c531d4e06a7.js and the new build
calls that chunk 135-b4d0965ad68280be.js, so the request 404ed. The
neighbouring 255-3d881dfa8c72bc56.js loaded fine, because that chunk
had not changed between the builds and kept its name. Half old and half
new, which is the exact thing the per-build cache exists to prevent.

It does prevent it, for files it holds. What it cannot prevent is a
chunk it never held. The cache fills by visiting, chunks are lazy, and
a deploy takes the old ones off the server, so from that moment any
chunk the old shell had not already cached is gone for good. The way
out is the update prompt, and the update prompt lives inside the app
that cannot boot.

So this is a way out that does not live there. An inline script in the
head, ahead of the app's own code, listening for that failure and only
that one: drop the caches, unregister the worker, reload once. Explicit
updates stay, because they are a good decision. This speaks up only
when the shell is already broken and there is nothing left to protect.

Twice in ten minutes and it stops. If clearing the cache did not help
then the fault is on the server, and a page that reloads forever is one
broken page turned into a machine hammering it.

Inline rather than a component, because by the time React could mount a
component the chunk it needs may be the missing one. And serialised
from the real functions with toString() rather than the same rules
typed out again as a string, because two copies of a rule is how one of
them gets fixed. Eighteen tests, five of which run the serialised source
in a sandbox with the browser stubbed out, since source that has never
been run has never been checked.

Verified in the built output: the listener is in the HTML of every
prerendered page.
@vercel

vercel Bot commented Sep 17, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Arun's projects Team on Vercel.

A member of the Team first needs to authorize it.

@suskozaver
suskozaver merged commit 2b0e3d2 into ArunNGun:beta Sep 17, 2026
1 of 2 checks passed
@suskozaver
suskozaver deleted the fix/a-broken-shell-repairs-itself branch September 17, 2026 17:11
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.

1 participant