Skip to content

shouldSkipRedirect incorrectly skips cross-domain redirects when pathnames match #941

@devinle

Description

@devinle

Describe the bug

The shouldSkipRedirect function in fetchRedirect.ts incorrectly skips valid cross-domain redirects when the pathname of the original request matches the pathname of the redirect destination. This causes 404 errors instead of proper redirects when using Safe Redirect Manager, Yoast redirects, or any redirect plugin configured to redirect to an external domain with the same path.

Location: packages/core/src/utils/fetchRedirect.ts, lines 24-37

Problematic code:

function shouldSkipRedirect(link: string, redirect: string, sourceUrl: string) {
  const linkURL = new URL(link, sourceUrl);
  const redirectURL = new URL(redirect, sourceUrl);

  if (skipURLs.some((path) => redirectURL.pathname.includes(path))) {
    return true;
  }

  const linkParams = linkURL.searchParams;
  const redirectParams = redirectURL.searchParams;

  linkParams.sort();
  redirectParams.sort();

  return (
    linkURL.pathname.replace(/\/$/, '') === redirectURL.pathname.replace(/\/$/, '') &&
    linkParams.toString() === redirectParams.toString()
  );
}

The issue occurs because:

  1. fetchRedirect makes a HEAD request to WordPress and receives a 302 redirect to an external domain (e.g., https://www.publix.com/recipe/my-recipe/)
  2. removeSourceUrl is called, which strips the external domain and returns just the pathname (/recipe/my-recipe/)
  3. shouldSkipRedirect compares only the pathnames: /recipe/my-recipe/ === /recipe/my-recipe/
  4. Since pathnames match, shouldSkipRedirect returns true and the redirect is incorrectly skipped
  5. The function throws "Unable to redirect" and returns { location: null, status: 0 }
  6. The frontend displays a 404 error instead of following the redirect

Steps to Reproduce

  1. Set up a WordPress multisite with HeadstartWP
  2. Install Safe Redirect Manager (or any redirect plugin)
  3. Configure a redirect:
    • From: /recipe/my-recipe/ (on subsite, e.g., espanol.example.com)
    • To: https://www.example.com/recipe/my-recipe/ (external domain, same path)
    • Status: 302
  4. Configure HeadstartWP with redirectStrategy: '404' in headstartwp.config.js
  5. Visit https://espanol.example.com/recipe/my-recipe/ on the Next.js frontend
  6. Expected: User is redirected to https://www.example.com/recipe/my-recipe/
  7. Actual: User sees a 404 error page

Expected behavior

Cross-domain redirects should be followed regardless of whether the pathname matches. The shouldSkipRedirect function should compare full URLs (including hosts) or explicitly allow cross-domain redirects.

Proposed fix

Add a host comparison check before comparing pathnames. If the redirect is to a different host, it should never be skipped:

function shouldSkipRedirect(link: string, redirect: string, sourceUrl: string) {
  const linkURL = new URL(link, sourceUrl);
  const redirectURL = new URL(redirect, sourceUrl);

  if (skipURLs.some((path) => redirectURL.pathname.includes(path))) {
    return true;
  }

  // Cross-domain redirects should never be skipped
  if (linkURL.host !== redirectURL.host) {
    return false;
  }

  const linkParams = linkURL.searchParams;
  const redirectParams = redirectURL.searchParams;

  linkParams.sort();
  redirectParams.sort();

  return (
    linkURL.pathname.replace(/\/$/, '') === redirectURL.pathname.replace(/\/$/, '') &&
    linkParams.toString() === redirectParams.toString()
  );
}

Alternative fix: Preserve the full URL in fetchRedirect before calling removeSourceUrl, and pass the original redirect URL to shouldSkipRedirect for proper comparison.

Screenshots, screen recording, code snippet

Debug log showing the issue:

When config.debug.redirects is enabled, you can see:

REDIRECT https://publix.test/spanish-en/recipe/my-recipe/ {
  status: 302,
  location: 'https://www.publix.com/recipe/my-recipe/'
}

But the redirect is skipped because after removeSourceUrl:

  • link = /recipe/my-recipe/
  • redirect = /recipe/my-recipe/
  • Pathnames match → redirect skipped → 404 returned

Environment information

  • HeadstartWP Version: 1.3.3
  • Node Version: 20.x
  • Next.js Version: 15.x

WordPress information

  • WordPress Version: 6.7.x
  • PHP Version: 8.2
  • Safe Redirect Manager: 2.2.x (issue also affects Yoast redirects and other redirect plugins)
  • Multisite: Yes (but issue also affects single-site with cross-domain redirects)

Additional context

This bug affects any scenario where:

  • A redirect is configured from one domain to another
  • The pathname on both domains is identical
  • redirectStrategy: '404' is used in HeadstartWP config

Common use cases affected:

  • Consolidating content from subsites to a main site
  • Migrating translated content between language-specific domains
  • Redirecting draft/unpublished content to canonical URLs on production

Workaround

As a workaround, we implemented a custom error handler that:

  1. Pre-fetches SRM redirect data from app settings
  2. Manually checks for cross-domain redirects before falling back to HeadstartWP's handleError
  3. Makes a separate HEAD request that properly handles cross-domain redirects

This workaround is functional but adds complexity and additional network requests.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions