fix(core): fold the build into the route cache validator - #2374
Open
danielmlr wants to merge 1 commit into
Open
Conversation
🦋 Changeset detectedLatest commit: 08766d5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Contributor
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
`CacheHint.lastModified` carries the content row's `updated_at`, and Astro emits it as the response `Last-Modified`. The response also depends on the build, because `/_astro/*` filenames are content-hashed and a deployment only serves its own. After a deploy that changes only code the validator is unchanged, so a returning visitor's conditional request is answered with 304 and the browser keeps HTML referencing assets the new deployment no longer has — 404 on Workers Assets, leaving the page without CSS or JavaScript. A response-derived ETag would be the obvious remedy, but Cloudflare strips `ETag` from Worker HTML responses, so `Last-Modified` has to carry it. The middleware now folds a build timestamp, exported from a new `virtual:emdash/build` module, into the validator for on-demand responses. Astro keeps the later of two dates, so a route's own hint still wins whenever content is newer. Prerendered pages stay untouched — the host's static layer manages its own validators. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
danielmlr
force-pushed
the
fix/cache-validator-build-dimension
branch
from
August 8, 2026 13:48
afcb762 to
08766d5
Compare
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.
What does this PR do?
Closes #2369
CacheHint.lastModifiedcarries the content row'supdated_at, and Astro emits it as the responseLast-Modified. The response also depends on the build:/_astro/*filenames are content-hashed, and a deployment only serves its own. After a deploy that changes only code, the validator is unchanged — a returning visitor'sIf-Modified-Sinceis answered with 304, and the browser keeps HTML referencing assets the new deployment no longer has. On Workers those requests 404 and the page renders without CSS or JavaScript. Full reproduction and root-cause analysis in #2369.The obvious remedy — a response-derived
ETag— is unavailable on the primary platform: Cloudflare stripsETagfrom Worker HTML responses (verified in the issue), soLast-Modifiedhas to carry the build dimension too.The fix: a new
virtual:emdash/buildmodule exports a build timestamp (captured once per Vite plugin instance, so client/server passes agree), and the middleware folds it into the route cache validator viacache.set({ lastModified })beforenext(). Verified against Astro'sAstroCachesemantics:next()matters:set()clears a previousset(false), so applying it after the route would silently undo a route's explicit opt-out. A test pins this ordering.lastModifiednever activates caching — Astro only emits headers whenmaxAgeor tags are set. Sites that don't useAstro.cachesee no behavior change.I first tried deriving the deploy time from
CF_VERSION_METADATA, but that binds core tocloudflare:workersand requires a binding no template configures. The build timestamp is adapter-agnostic and fixes the same bug on Node deployments.Trade-off, deliberately accepted for minimality: a pure build timestamp invalidates on every deploy, including deploys where the
/_astro/*hashes did not change — cost is one full 200 instead of a 304 per returning visitor per deploy. A hash over the asset manifest would be more precise but couples core to adapter internals. If you consider core the wrong layer for this altogether — the hint arguably describes content correctly, and it is Astro that promotes it to a response header — say so and I'll turn this into a docs note with the middleware workaround from #2369 instead.Not included (possible docs follow-up): without any
Cache-Controlon HTML, browsers fall back to RFC 9111 heuristic freshness and may not revalidate for hours; a default header is a behavior change for every site and deserves its own discussion.Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change) — fullpackages/coreunit suite: 3532 passed; the one failure (virtual-modules.test.ts > watches resolved sandbox plugin entries) also fails on a cleanmaincheckout in my environment (macOS tmpdir) and is untouched by this PRpnpm formathas been runAI-generated code disclosure
Screenshots / test output
The reproducing test, on
mainbefore the fix:After the fix: