remix-run/react-router · error · Error
The `request.url` host does not match `origin` header from a
Error message
The `request.url` host does not match `origin` header from a forwarded action request. Aborting the action.
What it means
CSRF guard in `throwIfPotentialCSRFAttack`: the parsed `Origin` host differs from `request.url`'s host AND the origin is not in `allowedActionOrigins` (which supports wildcard patterns like `*.example.com`). The library refuses to run the action because it looks like a cross-site request forgery.
Source
Thrown at packages/react-router/lib/actions.ts:23
let originHeader = request.headers.get("origin");
let originDomain: string | null = null;
try {
originDomain =
typeof originHeader === "string" && originHeader !== "null"
? new URL(originHeader).host
: originHeader;
} catch {
throw new Error(
`\`origin\` header is not a valid URL. Aborting the action.`,
);
}
let host = new URL(request.url).host;
if (originDomain && originDomain !== host) {
if (!isAllowedOrigin(originDomain, allowedActionOrigins)) {
// This seems to be an CSRF attack. We should not proceed with the action.
throw new Error(
"The `request.url` host does not match `origin` header from a forwarded " +
"action request. Aborting the action.",
);
}
}
}
// Implementation of micromatch by Next.js https://github.com/vercel/next.js/blob/ea927b583d24f42e538001bf13370e38c91d17bf/packages/next/src/server/app-render/csrf-protection.ts#L6
function matchWildcardDomain(domain: string, pattern: string) {
const domainParts = domain.split(".");
const patternParts = pattern.split(".");
if (patternParts.length < 1) {
// pattern is empty and therefore invalid to match against
return false;
}
if (domainParts.length < patternParts.length) {View on GitHub (pinned to 1fd704a7da)
Solutions
- Add the offending origin host (or a wildcard like `*.example.com`) to the `allowedActionOrigins` config for the framework/server runtime.
- If the mismatch is unintentional, align the deployed app host with the host the browser sends in Origin.
- Verify the wildcard syntax (`*.example.com` not `*example.com`); see `matchWildcardDomain` for accepted semantics.
- Confirm the request really originates from your own UI before allowlisting.
Example fix
// before
export default {
// no allowedActionOrigins
};
// after (vite.config / config)
export default {
unstable_allowedActionOrigins: ['app.example', '*.preview.example.com'],
}; Defensive patterns
Strategy: validation
Validate before calling
const ALLOWED = ['app.example', '*.preview.example.com'];
function matchesWildcard(domain: string, pattern: string) {
const d = domain.split('.'), p = pattern.split('.');
while (p.length) {
const pp = p.pop(), dp = d.pop();
if (pp === '*' && dp) continue;
if (pp !== dp) return false;
}
return d.length === 0;
}
function isAllowedOrigin(host: string) {
return ALLOWED.some((p) => p === host || matchesWildcard(host, p));
} Try / catch
try {
await callAction({ request });
} catch (e) {
if (e instanceof Error && e.message.includes('host does not match `origin`')) {
return new Response('CSRF check failed', { status: 403 });
}
throw e;
} Prevention
- Keep `allowedActionOrigins` in sync with every domain/subdomain your UI is deployed to.
- Prefer wildcard entries for predictable subdomain families.
- Re-test CSRF after adding a new deployment domain.
When it happens
Trigger: A mutation request whose `Origin` host differs from the host of `request.url`, when no matching wildcard or exact entry exists in `allowedActionOrigins` (configured via `vite.config` `future.unstable_allowedActionOrigins` / server runtime config).
Common situations: App served from `app.example` but actions invoked from a different subdomain; deploying behind a domain you forgot to allowlist; a real CSRF attempt; a form submit from a staging site against a production API.
Related errors
AI-assisted analysis of remix-run/react-router@1fd704a7da (2026-08-12).
Data as JSON: /api/errors/4de215ceca989e60.
Report an issue: GitHub.