{"record":{"id":"b835cb3dbc464286","repo":"remix-run/react-router","slug":"the-request-url-origin-does-not-match-origin-h","errorCode":null,"errorMessage":"The `request.url` origin does not match `origin` header from a forwarded action request. Aborting the action.","messagePattern":"The `request\\.url` origin 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":29,"sourceCode":"      originUrl = new URL(originHeader);\n      originDomain = originUrl.host;\n    } else {\n      originDomain = originHeader;\n    }\n  } catch {\n    throw new Error(\n      `\\`origin\\` header is not a valid URL. Aborting the action.`,\n    );\n  }\n  let requestUrl = new URL(request.url);\n  let originMatchesRequest = originUrl\n    ? originUrl.origin === requestUrl.origin\n    : originDomain === requestUrl.host;\n\n  if (originDomain && !originMatchesRequest) {\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` origin 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":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/remix-run/react-router/blob/c091832969928593bf9f7d56d1b371bd0f6d5412/packages/react-router/lib/actions.ts#L11-L47","documentation":"React Router adds a CSRF-protection layer for mutation requests (POST/PUT/PATCH/DELETE) to UI routes: before running an action, throwIfPotentialCSRFAttack (packages/react-router/lib/actions.ts:1) parses the browser's `origin` header and compares its host against the host in `request.url`. If they differ and the origin host is not matched by the `allowedActionOrigins` config (exact host or `*`/`**` wildcard), the action is aborted with this error. In the built-in document and single-fetch handlers the throw is converted into a 400 Bad Request; in RSC/custom-server paths it surfaces via onError or rejection.","triggerScenarios":"A POST/PUT/PATCH/DELETE submitted to a UI route where request.headers.get(\"origin\") has a host that differs from new URL(request.url).host, e.g. the origin header says https://www.example.com but request.url is http://localhost:3000/... — and \"www.example.com\" is not in allowedActionOrigins. Concretely: (1) form/fetcher submissions or server actions issued from a page served on a different domain than the one the server thinks it is on; (2) a reverse proxy (nginx, ALB, Cloudflare, ngrok) that does not preserve the Host header, so the adapter builds request.url with an internal host; (3) requests carrying `origin: null` (sandboxed iframes, privacy-mode browsers, cross-origin redirects), since the literal string \"null\" never equals the request host; (4) staging/preview deployments on subdomains that were never allowlisted.","commonSituations":"Deploying @react-router/serve or @react-router/express apps behind a reverse proxy without `trust proxy` enabled — a documented \"breaking bug fix\" in the React Router changelog notes the check now compares against the host in the request URL instead of headers. Also common after upgrading React Router versions that introduced/changed this CSRF check (PR #14708, #14722), testing a local dev server through a tunnel (ngrok/localtunnel), embedding or posting from another owned domain (micro-frontends, marketing-site forms), or forgetting that preview URLs (Vercel/Netlify deploys) are different origins.","solutions":["If the cross-origin submission is legitimate, add the posting host to `allowedActionOrigins` in react-router.config.ts: allowedActionOrigins: [\"www.example.com\", \"*.example.com\"] — `*` matches one subdomain segment, `**` matches multiple.","If the app should be same-origin, fix the proxy/adapter so request.url carries the public host: enable `trust proxy` in @react-router/express (app.set(\"trust proxy\", true)) and make the proxy forwards Host / X-Forwarded-Host and X-Forwarded-Proto.","Need the value per environment? Set it at runtime on the ServerBuild in your custom server instead of the static config: return { ...build, allowedActionOrigins: [\"staging.example.com\"] } from the build getter passed to createRequestHandler.","Eliminate `origin: null` senders: remove `sandbox` attributes that opaque-ify the iframe origin, or move the form/action to be served from the same origin.","After upgrading React Router, smoke-test every mutation request (form POST, fetcher.submit, server action) in the deployed environment, since the CSRF check behavior changed across versions."],"exampleFix":"// before: every POST behind the proxy returns 400 \"Bad Request\"\n// react-router.config.ts\nexport default {\n  // no allowedActionOrigins -> only exact request.url host is accepted\n} satisfies Config;\n\n// after: option A - allowlist the public hosts (react-router.config.ts)\nexport default {\n  allowedActionOrigins: [\"www.example.com\", \"*.example.com\"],\n} satisfies Config;\n\n// after: option B - make request.url match reality (custom express server)\nexport const app = express();\napp.set(\"trust proxy\", 1); // honor X-Forwarded-Host/Proto from your proxy","handlingStrategy":"validation","validationCode":"// Before shipping, assert the server reconstructs request.url with the public host\n// and that every front-door host is allowlisted (custom express server example):\nimport { createRequestHandler } from \"@react-router/express\";\n\nconst PUBLIC_HOSTS = [\"www.example.com\", \"staging.example.com\"];\n\napp.use((req, res, next) => {\n  const host = req.hostname; // relies on trust proxy when behind a proxy\n  if (!PUBLIC_HOSTS.includes(host) && !host.endsWith(\".example.com\")) {\n    return res.status(400).send(\"Unexpected host\");\n  }\n  next();\n});\n\napp.use(\n  createRequestHandler({\n    build: async () => ({\n      ...(await import(\"virtual:react-router/server-build\")),\n      allowedActionOrigins: PUBLIC_HOSTS,\n    }),\n  }),\n);","typeGuard":null,"tryCatchPattern":"// Only relevant if you call the handler directly (e.g. matchRSCServerRequest\n// or createRequestHandler in a custom server); the built-in document and\n// single-fetch paths already convert this throw into a 400 response.\ntry {\n  response = await handler(request, loadContext);\n} catch (error) {\n  if (\n    error instanceof Error &&\n    error.message.includes(\"does not match `origin` header\")\n  ) {\n    return new Response(\"Bad Request\", { status: 400 });\n  }\n  throw error;\n}","preventionTips":["Keep `allowedActionOrigins` in react-router.config.ts (or on the ServerBuild) in sync with every domain that legitimately submits to the app, including staging and preview URLs.","Enable `trust proxy` / forward Host and X-Forwarded-* headers so the adapter builds request.url with the public host instead of an internal one.","Run one real mutation request (form POST or server action) against the deployed environment after every React Router upgrade and every proxy change — the CSRF check compares against the request URL host, which adapters control.","Avoid `origin: null` senders: do not load posting pages from sandboxed iframes or opaque origins; serve them from an allowlisted origin.","Never allowlist a bare wildcard like \"**\" in production — it defeats the CSRF check entirely; scope wildcards to your own domains (*.example.com)."],"tags":["csrf","security","origin-header","reverse-proxy","allowedactionorigins","action"],"backgroundTag":"csrf-origin-check-failed","analyzedSha":"c091832969928593bf9f7d56d1b371bd0f6d5412","analyzedAt":"2026-08-21T18:46:20.427Z","contentChangedAt":"2026-08-21T18:46:20.427Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}