From 1e62299db950caf30666468f9c596138060b4063 Mon Sep 17 00:00:00 2001 From: devinle Date: Mon, 2 Feb 2026 11:57:24 -0800 Subject: [PATCH] fix: shouldSkipRedirect incorrectly skips cross-domain redirects Add host comparison check in shouldSkipRedirect to ensure cross-domain redirects are never skipped, even when pathnames match. This fixes an issue where redirects configured from one domain to another with the same pathname would result in 404 errors instead of proper redirects. Fixes #941 Co-Authored-By: Claude Opus 4.5 --- .changeset/fix-cross-domain-redirect-skip.md | 5 +++++ packages/core/src/utils/__tests__/fetchRedirect.ts | 14 ++++++++++++++ packages/core/src/utils/fetchRedirect.ts | 5 +++++ packages/core/test/server-handlers.ts | 10 ++++++++++ 4 files changed, 34 insertions(+) create mode 100644 .changeset/fix-cross-domain-redirect-skip.md diff --git a/.changeset/fix-cross-domain-redirect-skip.md b/.changeset/fix-cross-domain-redirect-skip.md new file mode 100644 index 000000000..84eca3685 --- /dev/null +++ b/.changeset/fix-cross-domain-redirect-skip.md @@ -0,0 +1,5 @@ +--- +"@headstartwp/core": patch +--- + +Fix: shouldSkipRedirect incorrectly skipping cross-domain redirects when pathnames match. Fixes #941 diff --git a/packages/core/src/utils/__tests__/fetchRedirect.ts b/packages/core/src/utils/__tests__/fetchRedirect.ts index 2342180fd..9d138a9ce 100644 --- a/packages/core/src/utils/__tests__/fetchRedirect.ts +++ b/packages/core/src/utils/__tests__/fetchRedirect.ts @@ -51,4 +51,18 @@ describe('fetchRedirect', () => { global.fetch = originalFetch; }); + + it('handles cross-domain redirect with same pathname', async () => { + const result = await fetchRedirect('/recipe/my-recipe/', 'http://example.com/'); + + expect(result.location).toBe('https://www.external-domain.com/recipe/my-recipe/'); + expect(result.status).toBe(302); + }); + + it('handles cross-domain redirect with different pathname', async () => { + const result = await fetchRedirect('/old-recipe/', 'http://example.com/'); + + expect(result.location).toBe('https://www.external-domain.com/new-recipe/'); + expect(result.status).toBe(301); + }); }); diff --git a/packages/core/src/utils/fetchRedirect.ts b/packages/core/src/utils/fetchRedirect.ts index 60cd0cfe0..defdcd658 100644 --- a/packages/core/src/utils/fetchRedirect.ts +++ b/packages/core/src/utils/fetchRedirect.ts @@ -28,6 +28,11 @@ function shouldSkipRedirect(link: string, redirect: string, sourceUrl: string) { return true; } + // Cross-domain redirects should never be skipped + if (linkURL.host !== redirectURL.host) { + return false; + } + const linkParams = linkURL.searchParams; const redirectParams = redirectURL.searchParams; diff --git a/packages/core/test/server-handlers.ts b/packages/core/test/server-handlers.ts index c390b9211..aa2897fba 100644 --- a/packages/core/test/server-handlers.ts +++ b/packages/core/test/server-handlers.ts @@ -36,6 +36,16 @@ const handlers = [ return res(redirect('http://example.com/redirect-test-missing-slash', 301)); }), + // Cross-domain redirect with same pathname + rest.head('http://example.com/recipe/my-recipe/', (req, res) => { + return res(redirect('https://www.external-domain.com/recipe/my-recipe/', 302)); + }), + + // Cross-domain redirect with different pathname + rest.head('http://example.com/old-recipe/', (req, res) => { + return res(redirect('https://www.external-domain.com/new-recipe/', 301)); + }), + rest.get(/\/test-endpoint/, (req, res, ctx) => { return res(ctx.json({ ok: true })); }),