Skip to content

fix(core): fold the build into the route cache validator - #2374

Open
danielmlr wants to merge 1 commit into
emdash-cms:mainfrom
danielmlr:fix/cache-validator-build-dimension
Open

fix(core): fold the build into the route cache validator#2374
danielmlr wants to merge 1 commit into
emdash-cms:mainfrom
danielmlr:fix/cache-validator-build-dimension

Conversation

@danielmlr

Copy link
Copy Markdown
Contributor

What does this PR do?

Closes #2369

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: /_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's If-Modified-Since is 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 strips ETag from Worker HTML responses (verified in the issue), so Last-Modified has to carry the build dimension too.

The fix: a new virtual:emdash/build module 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 via cache.set({ lastModified }) before next(). Verified against Astro's AstroCache semantics:

  • Astro keeps the later of two dates, so a route's own content hint still wins whenever content is newer than the build.
  • Running before next() matters: set() clears a previous set(false), so applying it after the route would silently undo a route's explicit opt-out. A test pins this ordering.
  • A bare lastModified never activates caching — Astro only emits headers when maxAge or tags are set. Sites that don't use Astro.cache see no behavior change.
  • Prerendered pages are skipped: the host's static layer manages its own validators (Workers Assets keeps ETags there).

I first tried deriving the deploy time from CF_VERSION_METADATA, but that binds core to cloudflare:workers and 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-Control on 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

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change) — full packages/core unit suite: 3532 passed; the one failure (virtual-modules.test.ts > watches resolved sandbox plugin entries) also fails on a clean main checkout in my environment (macOS tmpdir) and is untouched by this PR
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation — n/a, no admin UI changes
  • I have added a changeset
  • New features link to an approved Discussion — n/a, bug fix

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 5 (draft), Claude Fable 5 (review pass)

Screenshots / test output

The reproducing test, on main before the fix:

FAIL  tests/unit/astro/middleware-cache-validator.test.ts
  × raises a content-only validator to the build time
AssertionError: expected 1786120009000 to be 1786141609000

After the fix:

✓ tests/unit/astro/middleware-cache-validator.test.ts (5 tests)
✓ tests/unit/astro/integration/virtual-modules.test.ts (20 tests)

@changeset-bot

changeset-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 08766d5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet area/core size/L labels Aug 8, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2374

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2374

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2374

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2374

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2374

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2374

emdash

npm i https://pkg.pr.new/emdash@2374

create-emdash

npm i https://pkg.pr.new/create-emdash@2374

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2374

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2374

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2374

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2374

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2374

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2374

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2374

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2374

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2374

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2374

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2374

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2374

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2374

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2374

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2374

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2374

commit: 08766d5

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Overlapping PRs

This 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
danielmlr force-pushed the fix/cache-validator-build-dimension branch from afcb762 to 08766d5 Compare August 8, 2026 13:48
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.

cacheHint.lastModified tracks content only, so a code-only deploy serves 304 and leaves browsers on HTML referencing deleted /_astro/* files

1 participant