Skip to content

fix(redirects): prevent circular redirect loop when a slug rename is reverted#1992

Open
swissky wants to merge 1 commit into
emdash-cms:mainfrom
swissky:fix/auto-redirect-revert-loop
Open

fix(redirects): prevent circular redirect loop when a slug rename is reverted#1992
swissky wants to merge 1 commit into
emdash-cms:mainfrom
swissky:fix/auto-redirect-revert-loop

Conversation

@swissky

@swissky swissky commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes the circular redirect loop reported in #1986. When a published item's slug is renamed A → B, an auto-redirect A → B is created. Reverting the slug back (B → A) created B → A, while the chain-collapsing step turned the original redirect into the self-redirect A → A — leaving the page at slug A unreachable.

createAutoRedirect now:

  • deletes any existing redirect whose source matches the new (now live) URL after collapsing chains — the new URL serves real content again, so a redirect from it would only shadow the page, and
  • skips creating a redirect entirely when old and new URL are identical.

Rename chains stay collapsed: after A → B → C → A, both /b and /c point at /a, and /a has no redirect.

Closes #1986

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 a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Cursor + Fable 5

Screenshots / test output

Regression tests in redirect-repository.test.ts (fail on main, pass with this fix):

  • does not create a loop when a slug rename is reverted (#1986)
  • does not create a loop when reverting after a rename chain
  • is a no-op when old and new slug resolve to the same URL
 Test Files  3 passed (3)
      Tests  62 passed (62)

…reverted

When a slug changes A -> B, an auto-redirect A -> B is created. Reverting
the slug back (B -> A) previously created B -> A while chain collapsing
turned the original redirect into the self-redirect A -> A, leaving the
page at slug A unreachable.

createAutoRedirect now deletes any redirect whose source matches the new
(now live) URL after collapsing chains, and skips creating a redirect
when old and new URL are identical.

Fixes emdash-cms#1986
@changeset-bot

changeset-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: deaabe9

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

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd 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 area/core size/M review/needs-review No maintainer or bot review yet labels Jul 12, 2026
@swissky swissky added the bot:review Trigger an emdashbot code review on this PR label Jul 12, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: deaabe9

@emdashbot emdashbot Bot 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.

This is the right fix for the reported issue (#1986). The approach is sound: prevent the auto-redirect machinery from creating self-redirects when a slug rename is reverted by (1) deleting any redirect whose source is the now-live URL and (2) short-circuiting when the old and new URLs resolve to the same path. Both changes live in the correct place (RedirectRepository.createAutoRedirect), and the regression tests cover the exact failure modes described.

I checked:

  • The full diff and the changed repository/test files.
  • All callers of createAutoRedirect: only packages/core/src/api/handlers/content.ts, which ignores the return value, so the Redirect | null signature change is safe internally and is not exposed via a public package export.
  • The redirect middleware: it performs a single hop, so any remaining self-redirect would loop; the fix removes the self-redirect before it can be reached.
  • SQL safety: the new delete uses Kysely's parameterized builder (source = ${newUrl}).
  • The _emdash_redirects schema: there is an index on source, so the additional delete stays cheap.
  • The changeset: present, correctly names the emdash package, and describes the user-visible fix.

No blocking issues or AGENTS.md violations found. The tests added are exactly the reproducing cases a TDD bug fix should include.

@emdashbot emdashbot Bot 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.

This PR fixes the circular redirect loop reported in #1986 with a focused, architecture-appropriate change: it skips creating a redirect when the old and new URLs are identical, and after collapsing existing chains it deletes any redirect whose source is the newly-live URL. I read the diff, the redirect repository, the single handler caller (createSlugChangeRedirect), the redirect middleware, and the new tests.

The approach is sound because the redirect middleware runs before Astro content routes, so a redirect from a URL that now serves real content does shadow the page. The chain-collapsing logic and the early-return no-op are exactly the right places to intervene.

I did find one regression risk: the cleanup delete removes any redirect with source = newUrl, including manually created rules. The bug being fixed is caused by auto-redirects (auto = 1), and scoping the delete to auto-redirects is sufficient to break the loop. The current unscoped delete could silently remove user-created redirects that happen to share the restored URL, which is unexpected data loss during a slug change. The included tests only exercise auto-redirects, so they wouldn't catch this.

Otherwise the changeset is well-written, the return-type widening to Redirect | null is handled by the caller, and the regression tests cover the reported scenarios.

// The new URL serves live content again — any redirect from it would
// shadow the page. This also removes the self-redirect that chain
// collapsing produces when a rename A → B is reverted (A → A).
await this.db.deleteFrom("_emdash_redirects").where("source", "=", newUrl).execute();

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.

[needs fixing] The cleanup delete removes every redirect with source = newUrl, including user-created manual redirects. The #1986 loop is produced by collapsed auto-redirects (auto = 1), so the safe, minimal fix is to scope this delete to auto-redirects only. Deleting manual redirects during an unrelated slug change is silent data loss.

Suggested change
await this.db.deleteFrom("_emdash_redirects").where("source", "=", newUrl).execute();
// The new URL serves live content again — any auto-redirect from it would
// shadow the page. This also removes the self-redirect that chain
// collapsing produces when a rename A → B is reverted (A → A).
await this.db
.deleteFrom("_emdash_redirects")
.where("source", "=", newUrl)
.where("auto", "=", 1)
.execute();

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-review No maintainer or bot review yet labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core bot:review Trigger an emdashbot code review on this PR review/approved Approved; no new commits since size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slug rename + revert creates circular redirect loop

1 participant