Cache-bust the runtime loader, and say something when it stalls - #8
Merged
Merged
Conversation
Merging #7 left the site stuck on the loading overlay for anyone who had visited it before. The deploy was fine and a fresh browser loaded it without complaint; the fault was four hours old and sitting in everyone else's cache. Every file the .NET runtime loads is content-hashed by the SDK except dotnet.js itself, and dotnet.js is the file that names all the hashed ones. Pages serves it with max-age=14400 and has no way to say otherwise for one path, so a returning visitor booted a four-hour-old dotnet.js, asked for an assembly hash that deploy had replaced, took a 404 partway through boot and never got the overlay removed. It only bites when the .NET side changes, which is why it hid through three web-only deploys and surfaced on the first one to touch C#. It has been latent since the site was written. vite.config.ts now hashes dotnet.js into __FRAMEWORK_ID__ and wasm.ts loads it as dotnet.js?v=<id>. Hashing that particular file is the exact invalidation key: the hashed names are embedded in it, so the id changes when, and only when, something it loads does. The query is a cache key rather than a parameter - everything dotnet.js goes on to fetch resolves relative to itself, without the query, and is content-hashed already. Verified booting three times each under vite preview, a plain static server and the dev server, since a plain static server is what Pages is. That needed @types/node, because the config now reads a file. Separately: dotnet publish copies into its output without clearing it first, so every rebuild that changed the engine left the previous build's hashed files beside the current ones, and they shipped. Three copies of ProtoGen.Wasm had accumulated locally. Harmless bytes rather than a fault - the boot manifest names exactly one of them - but it grows without limit and makes the output a poor guide to what is actually loaded. build-wasm.mjs clears the publish directory first now.
The overlay had one thing to say and said it regardless: still loading, failed, and never coming all looked identical from the outside. The middle one is already handled - main.ts has always caught a rejected boot - but the third is not, and the third is what shipped last week. A missing runtime file does not reject: the loader waits for something that will never arrive, so no catch anywhere runs. Confirmed by serving a build with the main assembly deleted. The page produced no error at all and sat on the spinner until the browser gave up. So after ten seconds a line is added under the spinner - added, not substituted, because the spinner may still be telling the truth. It says the engine is large and compiles on the first visit, and that if it never finishes this browser may be holding an out-of-date copy, with a link that fetches a fresh one. The link carries a cache-busting query rather than calling reload(), which is free to serve back the same cached files that caused the problem; the argument that used to force otherwise has not done anything for years. A fresh index.html is enough on its own, because it names the current bundle, which asks for the current runtime. Deliberately not a diagnosis. From inside the page a slow connection and a loader waiting on a file that will never arrive are indistinguishable, and telling them apart would mean interposing on fetch and deciding which failed requests matter - for a message that is the same either way. If it is only a slow connection, nothing has been taken off the screen and the load carries on. Verified in headless Chrome against a healthy build and one with the main assembly removed: the first boots without ever showing the line, the second shows it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the site being stuck on the loading overlay after #7 for anyone who had visited before. A fresh browser was always fine, which is why CI and a clean incognito window both looked healthy.
What happened
Every file the .NET runtime loads is content-hashed by the SDK — except
dotnet.js, which is the file that names all the hashed ones. Pages serves it withmax-age=14400and offers no per-path control, so a returning visitor booted a four-hour-olddotnet.js, asked for an assembly hash that the deploy had replaced, and took a 404 partway through boot:The hash only moves when the .NET assembly does, so this hid through three web-only deploys and surfaced on the first one to touch C#. It has been latent since the site was written; #7 was the trigger, not the cause.
The fix
vite.config.tshashesdotnet.jsinto__FRAMEWORK_ID__, andwasm.tsloads it asdotnet.js?v=<id>. Hashing that particular file is the exact invalidation key — the hashed names are embedded in it, so the id changes when, and only when, something it loads does. The query is a cache key, not a parameter: everythingdotnet.jsgoes on to fetch resolves relative to itself, without the query, and is content-hashed already.Needed
@types/node, since the config now reads a file.Verification
Boot checked in headless Chrome, three runs on each of
vite preview, a plain static server, and the dev server — a plain static server being what Pages is:Worth recording: an early single run made it look like the query had broken the boot, and it had not — a cold first load can exceed the headless time budget and leave the overlay up, which looks identical to a failure unless you check which overlay text is showing. A real failure renders "Could not load the WebAssembly engine", because
main.tscatches it. Any smoke test worth having has to assert on that distinction and tolerate a cold start, or it will lie in both directions.Also
dotnet publishcopies into its output without clearing it, so every rebuild that changed the engine left the previous build's hashed files beside the current ones — and they shipped. Three copies ofProtoGen.Wasmhad accumulated locally. Harmless bytes rather than a fault, since the boot manifest names exactly one, but it grows without limit and makes the publish output a poor guide to what is actually loaded.build-wasm.mjsclears the directory first now.And a fallback for when it goes wrong anyway
A second commit adds a line under the spinner after ten seconds — added, not substituted, since the spinner may still be telling the truth.
Worth recording why the existing
try/catchnever fired: serving a build with the main assembly deleted produces no error at all. The loader waits for a file that will never arrive, nothing rejects, and no catch anywhere runs. That is the failure mode that shipped, and it was unreachable by the handling already in place.The line says the engine is large and compiles on first visit, and that if it never finishes this browser may be holding an out-of-date copy — with a link that fetches a fresh one. It is deliberately not a diagnosis: from inside the page a slow connection and a stalled loader are indistinguishable, and telling them apart would mean interposing on
fetchand deciding which failed requests matter, for a message that is identical either way.