{"record":{"id":"4de215ceca989e60","repo":"remix-run/react-router","slug":"the-request-url-host-does-not-match-origin-hea","errorCode":null,"errorMessage":"The `request.url` host does not match `origin` header from a forwarded action request. Aborting the action.","messagePattern":"The `request\\.url` host does not match `origin` header from a forwarded action request\\. Aborting the action\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/react-router/lib/actions.ts","lineNumber":23,"sourceCode":"  let originHeader = request.headers.get(\"origin\");\n  let originDomain: string | null = null;\n\n  try {\n    originDomain =\n      typeof originHeader === \"string\" && originHeader !== \"null\"\n        ? new URL(originHeader).host\n        : originHeader;\n  } catch {\n    throw new Error(\n      `\\`origin\\` header is not a valid URL. Aborting the action.`,\n    );\n  }\n  let host = new URL(request.url).host;\n\n  if (originDomain && originDomain !== host) {\n    if (!isAllowedOrigin(originDomain, allowedActionOrigins)) {\n      // This seems to be an CSRF attack. We should not proceed with the action.\n      throw new Error(\n        \"The `request.url` host does not match `origin` header from a forwarded \" +\n          \"action request. Aborting the action.\",\n      );\n    }\n  }\n}\n\n// Implementation of micromatch by Next.js https://github.com/vercel/next.js/blob/ea927b583d24f42e538001bf13370e38c91d17bf/packages/next/src/server/app-render/csrf-protection.ts#L6\nfunction matchWildcardDomain(domain: string, pattern: string) {\n  const domainParts = domain.split(\".\");\n  const patternParts = pattern.split(\".\");\n\n  if (patternParts.length < 1) {\n    // pattern is empty and therefore invalid to match against\n    return false;\n  }\n\n  if (domainParts.length < patternParts.length) {","sourceCodeStart":5,"sourceCodeEnd":41,"githubUrl":"https://github.com/remix-run/react-router/blob/1fd704a7dabcbe3ae09d7387b460e6acaba30ec1/packages/react-router/lib/actions.ts#L5-L41","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nexport default {\n  // no allowedActionOrigins\n};\n\n// after (vite.config / config)\nexport default {\n  unstable_allowedActionOrigins: ['app.example', '*.preview.example.com'],\n};","handlingStrategy":"validation","validationCode":"const ALLOWED = ['app.example', '*.preview.example.com'];\nfunction matchesWildcard(domain: string, pattern: string) {\n  const d = domain.split('.'), p = pattern.split('.');\n  while (p.length) {\n    const pp = p.pop(), dp = d.pop();\n    if (pp === '*' && dp) continue;\n    if (pp !== dp) return false;\n  }\n  return d.length === 0;\n}\nfunction isAllowedOrigin(host: string) {\n  return ALLOWED.some((p) => p === host || matchesWildcard(host, p));\n}","typeGuard":null,"tryCatchPattern":"try {\n  await callAction({ request });\n} catch (e) {\n  if (e instanceof Error && e.message.includes('host does not match `origin`')) {\n    return new Response('CSRF check failed', { status: 403 });\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["csrf","actions","security","allowed-origins"],"backgroundTag":null,"analyzedSha":"1fd704a7dabcbe3ae09d7387b460e6acaba30ec1","analyzedAt":"2026-08-12T13:54:57.804Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}