Skip to content

fix: revalidate stable-named build assets - #126

Merged
KyleJune merged 2 commits into
mainfrom
fix/stable-build-asset-cache
Sep 11, 2026
Merged

fix: revalidate stable-named build assets#126
KyleJune merged 2 commits into
mainfrom
fix/stable-build-asset-cache

Conversation

@KyleJune

Copy link
Copy Markdown
Member

Summary

Juniper served every /build/* file other than main.js with public, max-age=14400, including CSS entry points whose filenames never change. After a deployment a browser could pair new HTML and JavaScript with a stylesheet from the previous build for up to four hours. The cache policy is now keyed on whether the filename is fingerprinted: esbuild's name-XXXXXXXX.ext outputs (and their source maps) keep the four-hour lifetime, and every other build asset gets the same private, no-cache, must-revalidate, max-age=0 plus ETag treatment main.js already had.

Changes

  • src/server.tsx: one /build/* middleware that applies long-lived caching only to fingerprinted names and ETag revalidation to everything else (main.js, main.css, custom entry points such as styles/theme.css or workers/sw.js, and their source maps).
  • src/server.test.tsx: coverage for main.css, nested stable entry points and source maps, fingerprinted JS/CSS/map/route chunks keeping public, max-age=14400 with no ETag, and a conditional request round trip (If-None-Match → 304 with headers retained, mismatch → 200 with body).
  • docs/static-files.md, docs/styling.md, docs/deployment.md: state the actual defaults, drop the per-app ETag middleware recipe the framework now covers, add a migration section for URLs browsers already cached under the old policy (change the asset URL once), and note that CDN settings can override origin headers so verification belongs on the public custom domain.

Testing

  • deno task check and deno task test --parallel --reporter=dot green (33 files, 385 steps).
  • Regression proof: with the original middleware restored, the three new stable-asset tests fail at the Cache-Control and ETag assertions; the fingerprinted-asset test stays green on both.

Closes

Closes #125

🤖 Generated with Claude Code

KyleJune and others added 2 commits September 10, 2026 00:11
Only fingerprinted /build outputs (name-XXXXXXXX.ext) keep the four-hour
lifetime. Every other build asset, including CSS and custom entry points
with stable filenames, now gets the same ETag revalidation as main.js so a
deployment cannot pair new HTML and JavaScript with a stale stylesheet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every negative fixture was hyphen-free, so four widening mutations of
FINGERPRINTED_BUILD_ASSET passed the suite; the one that treats any
hyphenated stem as a hash would have served a stable theme-dark.css for
four hours. Adds hyphenated names that are not esbuild hashes, proves the
ETag tracks content, corrects the override example to match source maps,
and documents the one filename shape the heuristic misreads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@KyleJune

Copy link
Copy Markdown
Member Author

test: pin what the fingerprint rule rejects — adversarial review found the discriminating half of FINGERPRINTED_BUILD_ASSET was untested. Every negative fixture was hyphen-free, so four widening mutations passed the suite, including one that treats any hyphenated stem as a hash and would serve a stable theme-dark.css for four hours — the exact bug this PR fixes.

Added negatives that are hyphenated but not esbuild hashes (theme-dark.css, main-ABC123.css six chars, main-abcd2345.css lowercase, vendor-ABCD2345EF.js ten chars). Three of the four mutations now fail at the Cache-Control assertion for the specific fixture that distinguishes them; the fourth (dropping the $ anchor) still passes and is left unpinned deliberately, since no realistic esbuild output distinguishes it and a fixture for it would be contrived.

Also from the review: the 304 test now proves the ETag tracks content (rewrite the file, replay the old validator, expect 200 and the new body) rather than only that a matching validator returns 304. The override example in the static-files guide used /-[A-Z0-9]{8}\.[a-z0-9]+$/, which contradicts the framework rule stated above it by missing .map files, so a reader copying it got one-year-immutable JS with four-hour sourcemaps. The deployment guide now says private moves entry-point traffic to the origin rather than a CDN edge.

One item is a judgement call rather than a fix, left for you: the rule is a filename heuristic, so a stable entry point whose stem ends in eight uppercase alphanumerics (sw-REGISTER.js, Inter-VARIABLE.woff2) reads as fingerprinted and gets the four-hour lifetime. Tightening the character class does not help, since the real hashes observed use only A-Z and 2-7 and both examples still qualify. The exact discriminator is esbuild's metafile output list, which is a structural change. Documented the shape to avoid for now.

deno task check and deno task test --parallel --reporter=dot green (33 files, 386 steps).

@KyleJune
KyleJune merged commit 89a3466 into main Sep 11, 2026
10 checks passed
@KyleJune
KyleJune deleted the fix/stable-build-asset-cache branch September 11, 2026 03:38
KyleJune pushed a commit that referenced this pull request Sep 11, 2026
## [0.11.5](0.11.4...0.11.5) (2026-09-11)

### Bug Fixes

* revalidate stable-named build assets ([#126](#126)) ([89a3466](89a3466))
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.11.5 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use safe cache defaults for build assets with stable URLs

1 participant