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
- 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 `//`).
- If the location legitimately comes from an external source, resolve and validate it against your own origin before calling `redirect()`.
- Keep the exception path deliberate: catch this error in your request handler and respond 400 instead of letting the SSR request 500.
- 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
- Always normalize redirect targets to root-relative paths (single leading slash, never '//') before calling redirect().
- Never pass query parameters, DB fields, or upstream API values directly as redirect locations.
- For absolute redirects, verify new URL(target).origin equals the request origin.
- Add tests that submit javascript:, data:, and //evil.com targets to every redirecting route.
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
- Prerender (data): Received a ${response.status} status code
- Missing body in server response
- Failed to clone server response
- Invalid redirect location
- Invalid redirect location
AI-assisted analysis of remix-run/react-router@7aea711dd1 (2026-08-18).
Data as JSON: /api/errors/e97971758cee6146.
Report an issue: GitHub.