remix-run/react-router · error · Error

Invalid redirect location

Error message

Invalid redirect location

What it means

When a redirect thrown on the server crosses the RSC/error-digest boundary into client render, React Router decodes the digest, parses the redirect location, and validates its protocol against a blocklist (`javascript:`, `data:`, `blob:`, `file:`, `chrome-extension:`, etc. via `hasInvalidProtocol`). Throwing here is a deliberate security guard: silently honoring such a location would allow `javascript:` XSS through server-controlled redirects.

Source

Thrown at packages/react-router/lib/hooks.tsx:1177

    error &&
    "digest" in error &&
    typeof error.digest === "string"
  ) {
    let redirect = decodeRedirectErrorDigest(error.digest);
    if (redirect) {
      let existingRedirect = errorRedirectHandledMap.get(error);
      if (existingRedirect) throw existingRedirect;

      let parsed = parseToInfo(redirect.location, basename);
      let target = parsed.absoluteURL || parsed.to;
      validateNavigationTarget(
        redirect.location,
        target,
        getNavigatorCurrentUrl(navigator),
        "allow-explicit",
      );
      if (hasInvalidProtocol(target)) {
        throw new Error("Invalid redirect location");
      }

      if (isBrowser && !errorRedirectHandledMap.get(error)) {
        if (parsed.isExternal || redirect.reloadDocument) {
          window.location.href = target;
        } else {
          const redirectPromise: Promise<void> = Promise.resolve().then(() =>
            window.__reactRouterDataRouter!.navigate(parsed.to, {
              replace: redirect.replace,
            }),
          );
          errorRedirectHandledMap.set(error, redirectPromise);
          throw redirectPromise;
        }
      }

      return <meta httpEquiv="refresh" content={`0;url=${target}`} />;
    }

View on GitHub (pinned to 7aea711dd1)

Solutions

  1. Validate redirect targets against an allowlist before calling `redirect()`: only allow relative paths or `http:`/`https:` URLs.
  2. Prefer relative locations (`redirect(url.pathname + url.search)`) after parsing with `new URL(value, request.url)`.
  3. Reject or sanitize user-provided redirect inputs at the trust boundary (loader args, form fields).
  4. Audit any DB-stored or config-driven redirect destinations for unsafe schemes.

Example fix

// before
export async function loader({ request }: LoaderFunctionArgs) {
  const next = new URL(request.url).searchParams.get("next") ?? "/";
  return redirect(next);
}

// after
export async function loader({ request }: LoaderFunctionArgs) {
  const raw = new URL(request.url).searchParams.get("next") ?? "/";
  const url = new URL(raw, request.url);
  if (url.origin !== new URL(request.url).origin) throw redirect("/");
  return redirect(url.pathname + url.search);
}
Defensive patterns

Strategy: validation

Validate before calling

function safeRedirectTarget(raw: string, request: Request): string | null {
  try {
    const url = new URL(raw, request.url);
    return url.protocol === "http:" || url.protocol === "https:"
      ? url.pathname + url.search
      : null;
  } catch {
    return null;
  }
}

Type guard

const isSafeRedirect = (raw: string): boolean => {
  try {
    const p = new URL(raw, "http://x").protocol;
    return p === "http:" || p === "https:";
  } catch {
    return false;
  }
};

Prevention

When it happens

Trigger: `redirect("javascript:alert(1)")` or a redirect to a `data:` URL thrown from a loader/action/middleware; a redirect location built from user input (query param `?next=`) that contains an unsafe scheme; an attacker-supplied URL stored in a DB and used as a redirect target.

Common situations: Open-redirect style flows (`redirect(searchParams.get("next"))`) where the value is attacker-controlled; importing legacy URLs that embed `javascript:` links; penetration tests flagging redirect handling.

Related errors


AI-assisted analysis of remix-run/react-router@7aea711dd1 (2026-08-28). Data as JSON: /api/errors/92dd66ab913eca72. Report an issue: GitHub.