Skip to content

fix(auth): keep static file cache policy for signed-in users, and skip the Worker for hashed chunks - #552

Merged
Brooooooklyn merged 11 commits into
mainfrom
fix/static-asset-cache-policy
Oct 22, 2026
Merged

Brooooooklyn merged 11 commits into
mainfrom
fix/static-asset-cache-policy

Conversation

@Brooooooklyn

Copy link
Copy Markdown
Member

Signed-in users never cached JS or CSS, and every static file request started the Worker. A Codiff app on Void ran into this and worked around it with a post-build script. This PR fixes it in Void.

What was wrong

Signed-in GET /assets/<hash>.js   (pages / SSR / SPA with edge rules)
  auth middleware ─► next() ─► file served: "public, max-age=31536000, immutable"
        ◄── appendAuthHeaders copies getSession's "no-store" + "pragma: no-cache" over it
  result: no-store   (also for anonymous requests that carry any cookie, e.g. _ga=1)

The Worker also ran for every file, and for signed-in users each file cost one session read (D1).

Changes

1. Static files keep their own cache policy (all targets, runtime/)

request before after
no cookie public, …, immutable same
any cookie / signed in no-store private, max-age=31536000, immutable
session cookie refreshed on this request no-store private, no-store
page / API response for a signed-in user no-store same
  • The fallback marks responses that come from ASSETS (static-asset-marker.ts).
  • The auth middleware treats a response as a file only if user middleware left its Cache-Control, Set-Cookie and body stream unchanged. If middleware swapped the body after next(), the response is handled like a route again (no-store).
  • private, not public: user middleware may gate files on the session, so a shared cache must never keep one.

2. Hashed chunks skip the Worker (native void deploy --platform cloudflare only)

eligible build?  ── no ──► output unchanged
      │ yes
      ├─ dist/ssr/wrangler.json  run_worker_first: ['/**', '!/assets/a-123.js', '!/assets/b-456.js', …]
      └─ dist/client/_headers    one immutable rule per hashed chunk (Void marker on line 1)
  • Eligible means: no global middleware; no header, redirect, rewrite or fallback rules; no public _headers/_redirects; no route, page, WebSocket, prerender or revalidate pattern that can reach the assets folder; base /; 1–99 hashed chunks; not a managed (dispatch) build.
  • Rules are per file, not /assets/*. Misses, client routes like /assets/42, and custom 404 pages still reach the Worker.
  • The version-API upload sends the generated _headers as a text/plain module, as cf does. User-written _headers handling is unchanged.
  • Watch rebuilds keep _headers and run_worker_first in step. A stale generated _headers is removed.
  • Workers Free supports all of this (static requests are free).

Not in this PR

  • An opt-in that would let apps with middleware also skip the Worker (new API, and a security call for gated apps)
  • Turning on better-auth cookieCache by default (changes how quickly a revoked session stops working)
  • Carrying a D1 read-replica bookmark across requests
  • Managed (dispatch) bypass

Verification

  • New tests: asset-worker-bypass, client-asset-manifest, plugin-routing bypass wiring, cf-version-runner _headers module, auth-middleware (incl. swapped body), auth-isr-cache-policy with real better-auth against each fallback (incl. 304), routing-runtime marker, Miniflare bypass test (Worker does not run), ssr-playground build output.
  • vp run check: 33/33
  • Full vp test run: 732 files, 14403 tests passed, 0 failed
  • Repro wiring the real runtime: signed-in / cookie requests now get private, … and never no-store on files

🤖 Generated with Claude Code

Brooooooklyn and others added 11 commits October 22, 2026 20:37
Static files that the Worker serves from ASSETS ran inside the auth
middleware, which copied the session lookup's `no-store` and
`pragma: no-cache` onto them for any request with a cookie,
Authorization or CF Access header. Browsers then refetched hashed JS
and CSS on every navigation once a user signed in.

The Pages, SSR and asset-only fallbacks now record the Cache-Control of
the file they served on the Hono context. When the final response is
still that file (success status, same Cache-Control, no added
Set-Cookie), the auth middleware keeps the file's own policy and only
makes it private for credentialed requests, e.g.
`private, max-age=31536000, immutable`. A session cookie refresh on the
same request still gets `private, no-store`. Pages, API routes, and
files whose headers user middleware changed keep today's behaviour.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…native builds

Every request for a fingerprinted chunk started the Worker, ran the auth
middleware and, for signed-in users, a session read, only to copy the
asset response.

Native Cloudflare builds now patch the built wrangler.json to
`run_worker_first: ['/**', '!/<assetsDir>/*']` and write a Void-generated
`_headers` that lists each manifest-proven chunk with
`public, max-age=31536000, immutable`, so the asset layer keeps today's
cache policy. This applies only when every condition holds: no global
middleware, no header/redirect/rewrite/fallback rules, no public
`_headers`/`_redirects`, no route, page, WebSocket, prerender or
revalidate pattern that reaches the assets folder, base '/', 1-100 hashed
chunks, no user `_headers` in the client output, and not a managed build.
Otherwise the output is unchanged, and a stale generated `_headers` is
removed.

The pinned-secrets version upload sends the generated `_headers` as a
`_headers` text/plain module, as the Workers versions API and cf expect.
User-authored `_headers` files are still not sent on that path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A middleware that swaps c.res after next() keeps the file's Cache-Control
and ETag, so the auth middleware still saw an unchanged file and gave the
new body a cacheable private policy. Record the body stream with the
marker and require the final response to keep it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Cloudflare Vite plugin writes the built wrangler.json as compact JSON
with no trailing newline, so the indent and newline branches never ran.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
resolveAssetWorkerBypass now returns the final run_worker_first and the
_headers text, so the plugin no longer repeats its checks through two
exported helpers whose null results could not happen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A negative run_worker_first rule sends every matching request to the asset
layer, misses included, so '!/assets/*' took SSR rendering, the SPA shell,
the custom 404 page and the /assets redirect away from paths under the
assets folder. Emit one negative rule per immutable file instead, which
caps the bypass at 99 files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rebuilds

In vite build --watch a client-only rebuild empties dist/client but keeps
the Worker's wrangler.json, which still held the earlier bypass rule. The
plan then saw no lone '/**' and wrote nothing, leaving hashed chunks out of
the Worker with no immutable _headers. Treat the earlier rule as '/**', and
write '/**' back when the rebuild cannot use the bypass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add plugin-level cases for a route and a page under the assets folder,
redirect and fallback rules, a public _redirects file, and revalidate and
prerender paths under the folder. Each fails when its fact is dropped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e Worker

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ves them

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T12:55:47.722093Z 803a53b PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://fix-static-asset-cache-policy-void-cloud.voidzero-docs.workers.dev (commit 803a53b)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://84627d05-void-cloud.voidzero-docs.workers.dev 803a53b 2026-10-22T12:52:19.619Z Visit the dashboard ↗

@Brooooooklyn
Brooooooklyn merged commit 02e7818 into main Oct 22, 2026
20 checks passed
@Brooooooklyn
Brooooooklyn deleted the fix/static-asset-cache-policy branch October 22, 2026 13:05
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