remix-run/react-router · error · Error

Invalid redirect location

Error message

Invalid redirect location

What it means

Thrown by the RSC SSR server when it decodes a single-fetch redirect payload (server response with status 202, the SINGLE_FETCH_REDIRECT_STATUS) whose `location` uses a blocked protocol. `hasInvalidProtocol` matches Chrome's URL blocklist (`javascript:`, `data:`, `file:`, `blob:`, `about:`, `chrome:`, `chrome-untrusted:`, `content:`, `devtools:`, `filesystem:`), so the server refuses to copy that location into the `Location` header of the response it returns. This is a security guard against open-redirect/XSS via attacker-influenced redirect targets, not a generic parse failure.

Source

Thrown at packages/react-router/lib/rsc/server.ssr.tsx:208

    }) as DecodedPayload;
  };

  let renderRedirect: { status: number; location: string } | undefined;
  let renderError: unknown;

  try {
    if (!detectRedirectResponse.body) {
      throw new Error("Failed to clone server response");
    }
    const payload = (await createFromReadableStream(
      detectRedirectResponse.body,
    )) as RSCPayload;
    if (
      serverResponse.status === SINGLE_FETCH_REDIRECT_STATUS &&
      payload.type === "redirect"
    ) {
      if (hasInvalidProtocol(payload.location)) {
        throw new Error("Invalid redirect location");
      }

      const headers = new Headers(serverResponse.headers);
      headers.delete("Content-Encoding");
      headers.delete("Content-Length");
      headers.delete("Content-Type");
      headers.delete("X-Remix-Response");
      headers.set("Location", payload.location);

      return new Response(serverResponseB?.body || "", {
        headers,
        status: payload.status,
        statusText: serverResponse.statusText,
      });
    }

    let reactHeaders = new Headers();
    let status = serverResponse.status;

View on GitHub (pinned to 7aea711dd1)

Solutions

  1. Find the loader/action that produced the redirect and stop feeding it untrusted locations: allow-list same-origin relative paths (must start with a single `/`, not `//`).
  2. If the location legitimately comes from an external source, resolve and validate it against your own origin before calling `redirect()`.
  3. Keep the exception path deliberate: catch this error in your request handler and respond 400 instead of letting the SSR request 500.
  4. Add a regression test that sends `?redirectTo=javascript:...` and asserts a safe response.

Example fix

// before
export async function loader({ request }: LoaderFunctionArgs) {
  let to = new URL(request.url).searchParams.get("redirectTo") ?? "/";
  return redirect(to); // javascript: payload reaches the RSC SSR redirect path
}

// after
const SAFE = /^\/[^\/]/; // root-relative, not protocol-relative
export async function loader({ request }: LoaderFunctionArgs) {
  let to = new URL(request.url).searchParams.get("redirectTo") ?? "/";
  return redirect(SAFE.test(to) ? to : "/");
}
Defensive patterns

Strategy: validation

Validate before calling

const BLOCKED_PROTOCOLS = [
  "about:", "blob:", "chrome:", "chrome-untrusted:", "content:",
  "data:", "devtools:", "file:", "filesystem:", "javascript:",
];
function isSafeRedirectTarget(location: string): boolean {
  try {
    return !BLOCKED_PROTOCOLS.includes(new URL(location).protocol);
  } catch {
    return true; // relative paths don't parse; they're fine
  }
}
// before returning a redirect from a loader/action:
if (!isSafeRedirectTarget(to)) to = "/";

Type guard

function isRootRelativePath(loc: string): loc is `/${string}` {
  return /^\/[^\/]/.test(loc); // starts with '/', not '//'
}

Try / catch

try {
  return await handleDocumentRequest(request);
} catch (e) {
  if (e instanceof Error && e.message === "Invalid redirect location") {
    return new Response("Bad redirect target", { status: 400 });
  }
  throw e;
}

Prevention

When it happens

Trigger: A loader or action in an RSC (unstable React Server Components) app returns or throws `redirect()` (or a hand-built Response with a Location header) whose target parses to a blocked protocol; during SSR the framework clones the server response, `createFromReadableStream` decodes the RSC payload, sees `payload.type === "redirect"` with status 202, and the protocol check on `payload.location` fails.

Common situations: Passing a user-controlled query parameter (e.g. `?redirectTo=`) straight into `redirect()` and a malicious client sends `javascript:alert(1)` or `data:text/html,...`; a CMS or upstream API returning a `file:`/`data:` URL that the loader forwards; penetration-test payloads hitting SSO/login `returnTo` flows.

Related errors


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