Skip to content

fix(core): purge Workers Cache source paths when redirects are saved - #3982

Merged
danielmlr merged 1 commit into
mainfrom
bot/fix-3978
Oct 8, 2026
Merged

danielmlr merged 1 commit into
mainfrom
bot/fix-3978

Conversation

@emdashbot

@emdashbot emdashbot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Refs #3978.

The redirect write routes never invalidated the Astro/Workers route cache, so a cached 200 page at a redirect source path continued to be served after the redirect was created, updated, or deleted.

Changes

  • Added packages/core/src/cache/redirect-paths.ts to generate { path } invalidation specs for a redirect source path and its trailing-slash variant.
  • Wired cache.invalidate() into POST /_emdash/api/redirects, PUT /_emdash/api/redirects/:id, and DELETE /_emdash/api/redirects/:id.
  • Updates that change the source path now purge both the previous and new source paths.
  • Added packages/core/tests/unit/astro/redirect-routes-cache-invalidate.test.ts as a regression test.

Verification

  • New regression test passes (4 tests).
  • Related cache-invalidation and redirect handler tests pass (37 tests total).
  • pnpm typecheck is clean.
  • pnpm lint:quick reports 0 diagnostics.
  • pnpm run --filter emdash build succeeds.

A changeset is included for the emdash package.

Refs #3978.

Try the candidate against your own site with the preview build:

npm i https://pkg.pr.new/emdash@bot/fix-3978

Opened automatically by emdashbot. A maintainer must review before merge.

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)
  • 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 (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...
  • I have included screenshots below if this PR changes the UI

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: emdashbot + Kimi K2.7 Code

Screenshots / test output

@changeset-bot

changeset-bot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e275b5c

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

This PR includes changesets to release 12 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-test Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 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

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://bot-fix-3978.try.emdashcms.com, https://bot-fix-3978-emdash-playground.emdash-cms.workers.dev (commit e275b5c)

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://33364f86.try.emdashcms.com, https://33364f86-emdash-playground.emdash-cms.workers.dev e275b5c 2026-10-08T00:10:58.075Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://10155dd4.try.emdashcms.com, https://10155dd4-emdash-playground.emdash-cms.workers.dev 760d872 2026-10-07T21:36:48.996Z Visit the dashboard ↗

@pkg-pr-new

pkg-pr-new Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-test

npm i https://pkg.pr.new/@emdash-cms/plugin-test@3982

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-loader

npm i https://pkg.pr.new/@emdash-cms/registry-loader@3982

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/@emdash-cms/registry-moderation@3982

@emdash-cms/registry-verification

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

upgrade-emdash

npm i https://pkg.pr.new/upgrade-emdash@3982

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: e275b5c

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR targets the right problem — Workers Cache can serve a stale 200 at a redirect source path after the redirect is written — but the chosen purge mechanism is inconsistent with EmDash's cache model and almost certainly does not work on Cloudflare Workers.

Across the whole codebase (content, settings, menus, taxonomies, widget-areas) the Astro route cache is invalidated with cache.invalidate({ tags: [...] }), backed by CacheHint tags. The deployment docs also describe Cloudflare Workers Cache invalidation via cache.purge({ tags: [...] }). The redirect middleware even notes that a 404 cannot be purged by path because Astro only exposes tag-based handles. Yet this PR introduces cache.invalidate({ path }) and asserts it in tests without any evidence that the Astro/Cloudflare provider supports path-shaped invalidation. If it does not, the write routes run a no-op, the stale 200 survives, and the changeset over-promises a fix.

I also checked: packages/core/tsconfig.json excludes src/astro/**, so the claimed pnpm typecheck clean result does not cover the modified route files. The new helper file is checked, but the route code that consumes { path } is not.

Before merge the author should either (a) demonstrate that context.cache.invalidate({ path }) is supported by the pinned Astro/Cloudflare versions and actually evicts cached responses, or (b) switch to tag-based invalidation (e.g. emit a stable redirect/source tag and ensure rendered pages carry that tag, or piggy-back on whatever path tag the adapter already writes, if any). A regression test that only mocks cache.invalidate and asserts the exact object the code constructs will pass regardless of whether the provider accepts that shape, so it needs to exercise real invalidation or at least verify the contract against the provider's type.

const alt = toggledTrailingSlash(source);
if (alt) paths.add(alt);
}
return Array.from(paths, (path) => ({ path }));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] Returns { path } cache-invalidation specs, but EmDash's route-cache abstraction (and the Cloudflare Workers provider it targets) uses tag-based invalidation. Every other cache.invalidate() call in the codebase passes { tags: string[] }, CacheHint only has tags/lastModified, and the deployment docs show cache.purge({ tags: [...] }). If the provider does not accept { path } these specs are silently ignored and #3978 is not fixed. Confirm the Astro/Cloudflare cache API supports { path } or emit tags instead.

Suggested change
return Array.from(paths, (path) => ({ path }));
return Array.from(paths, (path) => ({ tags: [path] }));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep { path } here. The Cloudflare adapter tags every response with its path (astro-path:<pathname>), and invalidate({ path }) clears exactly that tag. { tags: [path] } wouldn't match any page.

~ 🤖 Claude Code (Opus 5.5)

Comment on lines +1 to +5
/**
* Cache invalidation specs for redirect source paths.
*
* Workers Cache tags cached pages by request pathname. When a redirect is
* written, purging the source path (and its trailing-slash variant) clears

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] The docstring claims "Workers Cache tags cached pages by request pathname", which contradicts the rest of the codebase. EmDash tags cached pages by collection/entry identifiers, settings, menus, taxonomies, etc. (see CacheHint, chrome-tags.ts, and the deployment docs). Public pages are not automatically tagged by their URL pathname here, so this premise is unfounded and should be fixed or removed.

const result = await handleRedirectCreate(db, body);
if (result.success && cache?.enabled) {
for (const spec of redirectSourceCachePaths(result.data.source)) {
await cache.invalidate(spec);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] Calls cache.invalidate(spec) with { path } objects. This shape is not used anywhere else in the codebase and is unlikely to be understood by the Cloudflare Workers cache provider. The same concern applies to the PUT and DELETE handlers in [id].ts.

const result = await handleRedirectUpdate(db, id, body);
if (result.success && cache?.enabled) {
for (const spec of redirectSourceCachePaths(before.data.source, result.data.source)) {
await cache.invalidate(spec);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] Calls cache.invalidate(spec) with { path } objects generated by redirectSourceCachePaths. The rest of the admin API invalidates by { tags }; the deployment docs only demonstrate tag-based cache.purge. Unless the Astro/Cloudflare adapter explicitly supports path-based invalidation, this will not evict the stale 200 page at the redirect source.

const result = await handleRedirectDelete(db, id);
if (result.success && cache?.enabled) {
for (const spec of redirectSourceCachePaths(before.data.source)) {
await cache.invalidate(spec);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] Same path-based cache.invalidate(spec) call as the PUT handler. If the provider only supports tags, the cached response at the deleted redirect's source path will not be purged.

Comment on lines +63 to +65
expect(response.status).toBe(201);
expect(invalidate).toHaveBeenCalledTimes(2);
expect(invalidate).toHaveBeenCalledWith({ path: "/posts/notes-on-simplicity" });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[needs fixing] This test (and the rest of the file) mocks cache.invalidate and asserts it was called with the exact { path } object the implementation constructs. That pattern cannot fail when the real API does not support { path }; it only restates the diff. Replace with a test that exercises actual eviction or asserts against the provider's contract (e.g., { tags }), per AGENTS.md: "A test must be able to fail on a real regression."

@danielmlr danielmlr self-assigned this Oct 7, 2026

@danielmlr danielmlr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main change looks right, but one thing needs fixing before this can merge.

First, please keep { path }. The earlier review suggested switching to { tags: [path] }, but that would break the fix. The Cloudflare adapter (@astrojs/cloudflare, 14.3.1 here and 14.3.4 latest) gives every response a tag made from its path, astro-path:<pathname>, and invalidate({ path }) clears exactly that tag. { tags: [path] } would clear a tag that no page has. The new tests are fine as they are too: all four fail if you undo the route changes.

What needs fixing: if clearing the cache fails, saving a redirect shows an error even though the redirect was saved. The await cache.invalidate(spec) call sits inside the route's try, after the redirect is already written to the database. So when invalidate throws, for example with TypeError: cache.purge is not a function on a Worker Preview, creating, editing and deleting a redirect all return a 500. The change is stored anyway, and creating the same redirect again then fails because it already exists. Please catch the error, log it with a [cache] prefix, and return the normal response. Add a test where invalidate throws and each route still returns 201 or 200 with the change saved. Only change those three calls, and leave { path } and the redirect handlers as they are.

Please leave these as they are: clearing the path both with and without a trailing slash, clearing both the old and the new source when a redirect is edited, and doing nothing for pattern sources like /blog/[slug], which don't have a single path to clear. The short window described in #3978, where other Worker instances still use the old list of redirects, is a separate problem and not part of this PR.

~ 🤖 Claude Code (Opus 5.5)

const result = await handleRedirectCreate(db, body);
if (result.success && cache?.enabled) {
for (const spec of redirectSourceCachePaths(result.data.source)) {
await cache.invalidate(spec);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If invalidate throws here, the error ends up in the catch below and the request returns a 500, even though handleRedirectCreate has already saved the redirect. Catch and log the error here so the request still succeeds. The same goes for editing ([id].ts:70) and deleting ([id].ts:100).

~ 🤖 Claude Code (Opus 5.5)

Comment thread .changeset/kind-tigers-smell.md Outdated
"emdash": patch
---

Fixes redirect saves so the cached page at the redirect source path is purged when the redirect is created, updated, or deleted. With Workers Cache enabled, the next request now reaches the worker and runs the redirect middleware instead of returning a stale 200 response.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: wording that starts with what a site owner actually notices.

Suggested change
Fixes redirect saves so the cached page at the redirect source path is purged when the redirect is created, updated, or deleted. With Workers Cache enabled, the next request now reaches the worker and runs the redirect middleware instead of returning a stale 200 response.
Fixes new and changed redirects being ignored for pages already in the Workers Cache. Saving a redirect for an exact path now clears the cached page at that path.

~ 🤖 Claude Code (Opus 5.5)

@emdashbot

emdashbot Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Completed

Revise redirect cache-invalidation candidate to catch and log edge purge failures so the API call still succeeds after the database mutation.

Outcome: Revised the PR #3982 candidate per the maintainer review feedback. The original redirect save routes now call cache.invalidate({ path }) for each affected source path (preserving the trailing-slash variant), and edge-purge failures are caught and logged so the redirect mutation still returns success.

Key changes:

  • Added `invalidateRedirectSourcePaths()` to `packages/core/src/cache/redirect-paths.ts`. It skips disabled caches, builds the `{ path }` specs for the supplied source paths, and catches/log failures instead of throwing.
  • Updated `packages/core/src/astro/routes/api/redirects/index.ts` (POST) and `[id].ts` (PUT/DELETE) to use the helper after a successful DB mutation.
  • Added regression tests that assert create/update/delete still return 201/200 when `cache.invalidate` rejects, and that each failure is logged with `console.error`.
  • Adopted the maintainer's suggested changeset wording.

Verification:

  • `pnpm format` passed.
  • `pnpm typecheck` passed.
  • `pnpm lint:quick` returned zero diagnostics.
  • `pnpm build` (root) completed successfully.
  • Focused tests passed: 7 redirect cache-invalidation tests and 21 chrome cache-invalidation tests (28 total).

Mode: revise

View live dashboard · Run: e55c799f-460c-403d-ae4e-eace95d73f6e

…are saved (#3978)

EmDash-Run: e55c799f-460c-403d-ae4e-eace95d73f6e
@emdashbot

emdashbot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Failed

A maintainer requested changes.

  • Install dependencies and build the repository

Outcome: I couldn't prepare the investigation workspace: Sandbox operation utils.createSession was interrupted while the runtime connection was closing

Failed stage: workspace

Mode: revise

View live dashboard · Run: 80918d6d-789d-419d-aa77-b753592f7f68

@bimsonz

bimsonz commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

@danielmlr On the separate problem at the end of your review: I've put that in #3997. On a fresh install it turned out to be more than a short window. With this PR alone, the purged page was cached again straight away by a Worker instance still on the old redirects, and the plain URL never returned 301 in my runs. With #3997 as well, it returned 301 within a second. #3997 doesn't touch any file in this PR.

@danielmlr danielmlr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failed-purge handling looks right. A failed purge is logged and the save still succeeds, and the three new tests fail with a 500 when the catch is taken out.

The remaining part of #3978, where Worker instances that still have the old redirect rules cache the page again, is in #3997.

Thanks @bimsonz for splitting it out and measuring it.

I changed Fixes #3978 and Closes #3978 in the description to Refs #3978, so merging this does not close the issue.

@danielmlr
danielmlr merged commit 8e1e849 into main Oct 8, 2026
46 checks passed
@danielmlr
danielmlr deleted the bot/fix-3978 branch October 8, 2026 12:05
@emdashbot emdashbot Bot mentioned this pull request Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants