Skip to content

Survive the 503 that made a healthy deployment look like an outage - #73

Merged
peopleworks merged 1 commit into
mainfrom
boot-resilience
Sep 1, 2026
Merged

Survive the 503 that made a healthy deployment look like an outage#73
peopleworks merged 1 commit into
mainfrom
boot-resilience

Conversation

@peopleworks

Copy link
Copy Markdown
Owner

A user reported the app was down. It was not.

What was actually wrong

All 53 framework assets serve 200 and every SHA-256 matches the boot manifest byte for byte — the file in the report included:

declared in the manifest sha256-gx2VPgq3h4wLjW7iWPZwsBNKDDRcGzmOad/1nUBl9nE=
served by GitHub Pages gx2VPgq3h4wLjW7iWPZwsBNKDDRcGzmOad/1nUBl9nE=

GitHub Pages answered one file with a passing 503. The browser hashed the error page instead of the assembly, got J5J7M+…, SRI blocked it, and Blazor abandoned startup for good.

One blip on any of fifty-three files bricked the load. And there was nothing to see: the failure happens before Blazor owns an error UI, so the visitor gets the loading circle and no message. That is why the report arrived as "the app is down" rather than "System.IO.Compression did not arrive" — the same lesson as the version in the desktop footer. A user can only report what the app is willing to tell them.

What this changes

wwwroot/js/boot.js, plus autostart="false" so it can start Blazor itself:

  1. Retry — three attempts with backoff; the retries use cache: 'reload', since asking the cache again for a body we already know is bad is useless.
  2. A panel that speaks, in English and Spanish, when the retry does not help. It names the file that never came, says the reader's text never left their machine, and offers to try again.

The integrity guarantee is kept, not traded away. Returning a Response from loadBootResource takes the check away from Blazor, so the manifest hash is handed to fetch and the browser enforces it instead. The runtime's own ES modules are left to the default loader — answering those with a Response breaks the import.

Verified by reproduction, not by argument

Against a local server that 503s that same asset, in a real browser:

Scenario Before After
Two 503s (the production case) spinner, forever boots — console prints the reporter's three lines verbatim, then recovers
503 that never yields spinner, forever the panel, naming System.IO.Compression….wasm, EN and ES
No failures boots boots, 0 console errors

One finding worth keeping: Blazor.start() does not reject when a boot file will not come — the failure surfaces as an unhandled rejection inside mono_download_assets. My first attempt hung the panel off a .catch that never runs; it was caught by testing the permanent-failure case, and the explanation is now written from the retry loop, which is what knows it is out of tries.

Tests

410 green. Seven new in WebBootTests, guarding only what breaks silently: script order (Blazor must exist before boot.js runs), that integrity is still handed to fetch (dropping it would fail nothing and quietly stop verifying every assembly), that the runtime modules stay out of the interceptor, and that the panel still speaks both languages.

Scope

Web only. The desktop host has its own index.html and loads from local disk, where there is no 503 to survive.

🤖 Generated with Claude Code

https://claude.ai/code/session_015PEbbiYSNPw7jE3LrPNhyF

A user reported the app was down. It was not: all 53 framework assets serve
200 and every SHA-256 matches the boot manifest byte for byte, the failing one
included. What happened is that GitHub Pages answered one file with a passing
503, the browser hashed the error page instead of the assembly, the integrity
check failed, and Blazor stopped for good.

There was nothing to see. The failure happens before Blazor owns an error UI,
so the visitor gets the loading circle and no message — which is why the report
arrived as "the app is down" rather than "System.IO.Compression did not
arrive". The same lesson as the version in the desktop footer: a user can only
report what the app is willing to tell them.

So boot.js retries a boot file three times before believing it, and explains
itself in both languages if the retry does not help, naming the file that never
came. It keeps the integrity guarantee rather than trading it away: returning a
Response from loadBootResource takes the check away from Blazor, so the
manifest hash goes to fetch instead and the browser enforces it.

Verified by reproduction, not by argument. Against a server that 503s that same
asset twice, the console prints the reporter's three lines verbatim and the app
now boots anyway. Against one that never yields, the panel appears in English
and in Spanish. Blazor.start() does not reject when a boot file will not come —
the failure surfaces as an unhandled rejection inside mono_download_assets — so
the explanation is written from the retry loop, which is what knows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015PEbbiYSNPw7jE3LrPNhyF
@peopleworks
peopleworks merged commit 9678954 into main Sep 1, 2026
4 checks passed
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