Skip to content

Set explicit Cache-Control headers for the WebAssembly site - #16

Open
turbobobbytraykov wants to merge 3 commits into
vnextfrom
btraykov/cache-headers
Open

turbobobbytraykov wants to merge 3 commits into
vnextfrom
btraykov/cache-headers

Conversation

@turbobobbytraykov

@turbobobbytraykov turbobobbytraykov commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Staging sends dotnet.js, blazor.boot.json, service-worker.js and uno-config.js without Cache-Control, only with an ETag and Last-Modified. Browsers then pick a freshness time on their own (Chrome uses 10% of the file's age), so after a deploy a returning visitor can get the new index.html together with an old loader or an old uno-config.js:

  • uno-config.js keeps its URL when its content changes. The bootstrapper writes it into package_<sha1>/ after it has computed the folder's hash.
  • The service worker can stop updating. Its install step reads blazor.boot.json with a plain fetch(). A stale copy names runtime files the deploy has already deleted, so the precache fails and the new worker never installs.
  • CloudFront (production): with its default cache policy (CachingOptimized), a response without Cache-Control is kept for 24 hours. I haven't checked the policy or the error-caching TTL this distribution uses. Both decide how long CloudFront holds no-cache responses and errors, so please confirm them before the production deploy.

This PR makes the policy explicit in web.config.

Policy

Files Cache-Control
Everything by default, including index.html (also served for / and the sample deep links), service-worker.js, manifest.webmanifest, _framework/dotnet.js, _framework/blazor.boot.json, package_<sha1>/uno-config.js and uno-assets.txt, and code-viewer/*.json no-cache: browsers and proxies may keep a copy but revalidate it on every use, which is a 304 while it's unchanged
Content-addressed files: everything else in package_<sha1>/, and _framework files with a 10-character fingerprint public, max-age=31536000, immutable
Errors (4xx and 5xx) no-store
  • How:
    • One <clientCache cacheControlMode="DisableCache"/> at the application root.
    • Outbound rules raise the content-addressed files to one year. They match the file IIS actually serves ({REQUEST_FILENAME}), so the .br copy and the plain file get the same policy. They apply only to 200, 206 and 304 responses.
  • Removed: the three <location path="index.html*"> blocks. Configuration follows the rewritten URL, so the root default now covers index.html and its copies.
  • Encoding rules: they now also check the status, so an IIS error page for a .br path no longer gets a Content-Encoding header.
  • No server-level unlock is needed: no allowedServerVariables, no <httpCompression>.

In the .NET 9 build of vnext 0dbf623, 517 of the 857 URLs get the one-year policy (407 in the package folder, 110 in _framework), and the other 340 stay no-cache. Counting the .br copies, that's 1,030 of the 1,378 files.

What the other repositories do

I read the caching rules in the web.config files of the IgniteUI and Infragistics repositories.

  • IgniteUI/api-docs is the most refined. It uses a no-cache default and outbound rules that apply only to successful responses, and this PR takes the same approach.
  • IgniteUI/blazor-grid-examples marks all of _framework immutable. That's correct there only because it is a .NET 10 app; on .NET 9, dotnet.js and blazor.boot.json have no fingerprint.
  • angular-demos has an immutable rule without a status check, so its error pages are cached for a year too. I saw this live on a staging 403.
  • Uno's Azure Static Web Apps template, which is also our wwwroot/staticwebapp.config.json, marks all of package_* immutable, including uno-config.js (Uno.Wasm.Bootstrap issues #1076 and #1090). This PR doesn't follow it.

Verified locally

I used IIS Express, configured to answer local requests like remote ones (custom error pages).

  • Headers: I checked 18 kinds of file, each requested with Brotli, gzip and no compression, plus conditional requests. Every response has the intended Cache-Control.
  • Chrome, after a simulated deploy that changed only uno-config.js:
    • Current config: Chrome reused the old uno-config.js (1,589 B) from its disk cache without asking the server.
    • This PR: Chrome fetched the new one (1,624 B), got 304s for dotnet.js and blazor.boot.json, and loaded the one-year files (bootstrap scripts, runtime, fonts) from disk without a request.

What doesn't change

  • Repeat visits send about as many requests as before: 166 on both configs, in the test above.
    • The .NET 9 runtime fetches its ~110 boot resources with cache: 'no-cache', so they are revalidated on every start whatever the headers say.
    • Shortly after a deploy, Chrome's heuristic already serves the package files from disk. With this PR, the 55 that a visit loads stay on disk for a year instead of being revalidated once that heuristic runs out.
  • The point of this PR is correctness: no stale config or loader after a deploy.

Related PRs

Checking after the deploy

base=https://staging.infragistics.com/uno-wasm-demos
pkg=$(curl -s "$base/" | grep -o 'package_[0-9a-f]\{40\}' | head -n 1)
for path in "" _framework/dotnet.js "$pkg/uno-config.js" "$pkg/uno-bootstrap.js" "$pkg/does-not-exist.js"; do
  printf '%-60s ' "/$path"
  curl -s -o /dev/null -D - -H 'Accept-Encoding: br' "$base/$path" | grep -i '^cache-control' || echo '(none)'
done

Expected, in order: no-cache, no-cache, no-cache, public, max-age=31536000, immutable, no-store. The _framework/dotnet.js line holds for .NET 9 only: on .NET 10 (#18) that file is dotnet.<hash>.js, which gets the one-year policy, and dotnet.js returns 404 with no-store.

🤖 Generated with Claude Code

Staging sent dotnet.js, blazor.boot.json, service-worker.js and uno-config.js
without Cache-Control, so browsers and CloudFront picked their own freshness and
could pair a new index.html with an old loader or config after a deploy.

Everything now defaults to no-cache (revalidate; a 304 while unchanged).
Content-addressed files - package_<sha1>/ except uno-config.js and uno-assets.txt,
and fingerprinted _framework files - get public, max-age=31536000, immutable, only
on 200/206/304. Errors get no-store. The index.html <location> blocks are covered
by the root default and are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	browser/SamplesBrowser/Platforms/WebAssembly/wwwroot/web.config
turbobobbytraykov added a commit that referenced this pull request Nov 11, 2026
Review follow-up: as #16 does for Brotli, the Gzip encoding rule now applies only to 200, 206 and 304 responses, so an IIS error page for a .gz path gets no Content-Encoding header.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants