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

  1. Add the offending origin host (or a wildcard like `*.example.com`) to the `allowedActionOrigins` config for the framework/server runtime.
  2. If the mismatch is unintentional, align the deployed app host with the host the browser sends in Origin.
  3. Verify the wildcard syntax (`*.example.com` not `*example.com`); see `matchWildcardDomain` for accepted semantics.
  4. 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

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.