[fix](trx-frontend-http): stop freezing the page in the browser cache #57

Merged
sjg merged 1 commits from fix/asset-cache-revalidation into main 2026-08-07 19:02:46 +02:00
Owner

Chasing a report that Edge still hides the whole map bar when "Hide Filters" is clicked, after that layout was fixed. Edge is Chromium, so a rendering difference against Chrome was already suspicious. It is not a rendering difference.

What the server tells browsers

index.html, every SPA route serving it, style.css, themes.css and every generated entry bundle (app.js, aprs.js, map-core.js, …) go through one response builder that sends:

Cache-Control: public, max-age=31536000, immutable

None of those URLs change when the bytes behind them do — only esbuild's shared chunks are content-addressed (chunk-<hash>.js), entry names are fixed. The ETag is build-stamped, but immutable says don't ask, and a year of max-age says the same thing to anything that ignores it.

So a browser that loaded the app once can go on serving that copy of the page, stylesheet and bundles indefinitely. A fix ships; the browser that happened to cache the old stylesheet keeps rendering the old behaviour; the browser that got hard-reloaded shows the fix. Which is exactly the shape of the report.

Fix

Asset Policy
chunk-<hash>.js immutable, 1 year — the name changes with the bytes
/vendor/opus-decoder-0.7.11.min.js immutable — version is in the URL
index.html and all its routes, style.css, themes.css, entry bundles, bandplan.json, /vendor/leaflet.js no-cache — ask, and get a 304

no-cache is not "do not cache": the copy is kept and revalidated, so the usual answer is a 304 with no body. Cost is one conditional request per asset per load.

Verification

  • Unit tests on the policy each asset gets, including that a real hashed chunk from the manifest is still immutable.
  • Endpoint tests through the actix app: the page, the stylesheet and app.js come back with no-cache and an ETag, and an unchanged style.css answers 304 under the same policy.
  • cargo test -p trx-frontend-http 59 passed; fmt and clippy clean.

About the Edge symptom

This is the most likely reason it survived the layout fix, but I could not confirm it here — there is no Edge on this machine, and the map bar renders correctly in Chromium in both states. A hard reload in Edge (Ctrl+Shift+R) will settle it in five seconds: if the bar behaves after that, it was the cache and this change stops it recurring. If it still hides the whole bar, it is a genuine layout problem and I will want the Edge version — the collapsed state leans on width: fit-content with both left and right anchored, which is exactly the kind of over-constrained absolute box where engines have historically differed, and I would rewrite it to something unambiguous rather than guess again.

Chasing a report that Edge *still* hides the whole map bar when "Hide Filters" is clicked, after that layout was fixed. Edge is Chromium, so a rendering difference against Chrome was already suspicious. It is not a rendering difference. ## What the server tells browsers `index.html`, every SPA route serving it, `style.css`, `themes.css` and every generated entry bundle (`app.js`, `aprs.js`, `map-core.js`, …) go through one response builder that sends: ``` Cache-Control: public, max-age=31536000, immutable ``` None of those URLs change when the bytes behind them do — only esbuild's shared chunks are content-addressed (`chunk-<hash>.js`), entry names are fixed. The ETag is build-stamped, but `immutable` says don't ask, and a year of `max-age` says the same thing to anything that ignores it. So a browser that loaded the app once can go on serving that copy of the page, stylesheet and bundles indefinitely. A fix ships; the browser that happened to cache the old stylesheet keeps rendering the old behaviour; the browser that got hard-reloaded shows the fix. Which is exactly the shape of the report. ## Fix | Asset | Policy | |---|---| | `chunk-<hash>.js` | `immutable`, 1 year — the name changes with the bytes | | `/vendor/opus-decoder-0.7.11.min.js` | `immutable` — version is in the URL | | index.html and all its routes, `style.css`, `themes.css`, entry bundles, `bandplan.json`, `/vendor/leaflet.js` | `no-cache` — ask, and get a 304 | `no-cache` is not "do not cache": the copy is kept and revalidated, so the usual answer is a 304 with no body. Cost is one conditional request per asset per load. ## Verification - Unit tests on the policy each asset gets, including that a real hashed chunk from the manifest is still immutable. - Endpoint tests through the actix app: the page, the stylesheet and `app.js` come back with `no-cache` and an ETag, and an unchanged `style.css` answers 304 under the same policy. - `cargo test -p trx-frontend-http` 59 passed; fmt and clippy clean. ## About the Edge symptom This is the most likely reason it survived the layout fix, but I could not confirm it here — there is no Edge on this machine, and the map bar renders correctly in Chromium in both states. **A hard reload in Edge (Ctrl+Shift+R) will settle it in five seconds:** if the bar behaves after that, it was the cache and this change stops it recurring. If it still hides the whole bar, it is a genuine layout problem and I will want the Edge version — the collapsed state leans on `width: fit-content` with both `left` and `right` anchored, which is exactly the kind of over-constrained absolute box where engines have historically differed, and I would rewrite it to something unambiguous rather than guess again.
sjg added 1 commit 2026-08-07 10:51:02 +02:00
[fix](trx-frontend-http): stop freezing the page in the browser cache
CI / lint (pull_request) Successful in 2m22s
CI / test (pull_request) Successful in 8m37s
CI / frontend (pull_request) Successful in 4m33s
CI / reuse (pull_request) Successful in 6s
CI / lint (push) Failing after 14m3s
CI / test (push) Successful in 8m12s
CI / frontend (push) Successful in 3m39s
CI / reuse (push) Successful in 6s
c8b6f2d536
index.html, the stylesheets and the entry bundles are all served from fixed
URLs and answered with `public, max-age=31536000, immutable`.  Nothing in
those URLs changes when the bytes behind them do, and immutable tells the
browser not to ask, so a client that visited once could go on running the
page it downloaded then — for a year, with the build-stamped ETag never
consulted.  That is how a layout fix ships and one browser still shows the
old behaviour while every other one has it: not a rendering difference, a
copy of last week's stylesheet.

Only the shared chunks are content-addressed — esbuild hashes their names —
so only they can be kept forever.  Everything served from a stable URL now
answers `no-cache`, which asks and gets a 304 in the ordinary case, at the
cost of one conditional request per asset per load.  Vendored files with a
version in the URL stay immutable; Leaflet, whose URL does not name its
version, revalidates with the rest.

Covered both ways: a unit test on the policy each asset gets, and endpoint
tests that the page, the stylesheet and app.js come back revalidating with an
ETag, and that an unchanged one answers 304 under the same policy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
sjg merged commit c8b6f2d536 into main 2026-08-07 19:02:46 +02:00
sjg deleted branch fix/asset-cache-revalidation 2026-08-07 19:02:46 +02:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sjg/trx-rs#57