Serve the web UI with cache headers so upgrades are picked up - #763
Open
cyberb wants to merge 1 commit into
Open
Serve the web UI with cache headers so upgrades are picked up#763cyberb wants to merge 1 commit into
cyberb wants to merge 1 commit into
Conversation
The device UI and the auth UI were served with no Cache-Control header at all, so browsers applied heuristic freshness (roughly 10% of the file's age) and served a stale index.html without ever revalidating. After a platform refresh a phone would render the whole previous app from cache. Compounding it, "try_files $uri $uri/ /index.html" also caught asset requests, so a chunk that no longer existed on disk came back as 200 text/html instead of 404. Every route is a lazy import(), so the cached old bundle painted the app list fine but pressing an app icon fetched a route chunk, got HTML, and the module loader rejected on MIME type - the click silently did nothing. Dark mode showed the same way: the cached bundle predated the theme, so the page stayed white until a manual refresh. index.html is now no-cache, so it always revalidates and turns into a cheap 304 via the existing ETag. Hashed assets under /assets/ are immutable for a year and a missing one returns 404 rather than the SPA shell. add_header in a location replaces inherited headers rather than adding to them, so the /assets/ blocks re-declare HSTS and CORS. The frontend recovers on its own too: vite:preloadError and router onError trigger a single reload, guarded by a sessionStorage flag so a genuinely broken deploy cannot loop. The flag clears once the router is ready so a later upgrade can self-heal again. Devices already holding a stale index.html need one last manual refresh before the new headers take effect; after that they revalidate forever. Generated config verified with nginx -t. Covered by nginx config unit tests, frontend unit tests for the reload guard, and integration tests asserting the headers and the 404 on both the device and auth UIs.
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.
Problem
Reported from a phone: after a platform upgrade the app list renders, but pressing an app icon does nothing, and dark mode shows as white. A manual refresh fixes both.
Two bugs compounding, confirmed against a live device:
1. No
Cache-Controlheader on anything. The device UI and auth UI were served with onlyLast-Modified/ETag. With no explicit directive browsers fall back to heuristic freshness (~10% of the file's age), soindex.htmlwas served from cache without ever asking the server:2. Missing assets returned
200 text/html.try_files $uri $uri/ /index.htmlcaught asset requests too:Every route in
router/index.jsis a lazyimport(). So the cached old bundle painted the main screen fine, but pressing an app icon fetched a route chunk that no longer existed on disk, got the SPA shell back, and the ES module loader rejected it on MIME type — a silent no-op. Dark mode was the same story: the cached bundle predated the theme feature, so the page stayed white.Fix
config/nginx/public.conf, for both the device UI and the auth/login UI:Cache-Control: no-cacheat server level soindex.htmlalways revalidates — a cheap 304 via the existing ETag/assets/location withpublic, max-age=31536000, immutableandtry_files $uri =404, so content-hashed files cache for a year and a genuinely missing chunk fails loudly instead of masquerading as HTMLadd_headerin a location replaces inherited headers rather than adding to them, so the/assets/blocks re-declare HSTS and CORS — without that they'd be silently dropped for every JS/CSS file.web/platform/src/util/staleAssets.js— client-side self-healing:vite:preloadErrorandrouter.onErrortrigger a single reload, guarded by asessionStorageflag so a genuinely broken deploy can't become a reload loop. The flag clears afterrouter.isReady()so a later upgrade can self-heal again.Tests
backend/nginx/public_config_test.go— asserts the generated config revalidates index, marks assets immutable, 404s missing assets, and keeps HSTS/CORS inside the/assets/blocks (the last one guards theadd_headerinheritance trap)web/platform/tests/unit/staleAssets.spec.js— 5 tests: error detection, reload-once, ignoring unrelated router errors, the Vite preload path, and re-arming after the flag clearstest/test.py— 6 integration tests assertingno-cacheon index,immutable+javascriptcontent-type on assets, and404on a missing asset, for both the device UI andauth.<domain>Generated config additionally verified with
nginx -t(syntax is ok/test is successful).Caveat
Devices already holding a stale
index.htmlwon't pick up the new reload handler, since they won't fetch the new bundle. Those need one final manual refresh — after which they revalidate forever.