SvelteKit: Cross-site POST form submissions are forbidden
SvelteKit compares the Origin header with the origin it believes it is serving and rejects the mismatch, which is its CSRF protection rather than a bug. It appears behind a proxy that does not pass the original host and protocol through, or when a form genuinely posts to another host, such as a webhook receiver.
Quick fix
Read the commands before running them. Anything that restarts a service, deletes data or changes permissions should be tried on a non-production system first.
# The check compares two values, so print both before changing anything
curl -i -X POST https://app.example.com/login \
-H 'origin: https://app.example.com' -d 'user=a'
# adapter-node: tell it what the public origin is
ORIGIN=https://app.example.com node build
# or let it trust the proxy headers
# PROTOCOL_HEADER=x-forwarded-proto HOST_HEADER=x-forwarded-host node build
# The proxy has to actually send them
# proxy_set_header X-Forwarded-Host $host;
# proxy_set_header X-Forwarded-Proto $scheme;
// A route that is genuinely cross origin belongs in +server.js, where the
// form action check does not apply, rather than switching the check off
// svelte.config.js: leave this on in production
kit: { csrf: { checkOrigin: true } }
# Local development over a tunnel or a LAN address hits it too, so set
# ORIGIN to whatever the browser is really using
How to diagnose Svelte errors
Svelte errors mostly arise at the compiler's boundaries: the $ store prefix only works on real stores in component context, and SvelteKit's load functions run in two different environments with different capabilities. A 500 from a load function usually means server-only code (a database client, a secret, fs) reached a universal load that also runs in the browser.
If the quick fix above does not resolve it, work through these steps. They apply to this whole class of error, not just to this one message, which is usually what saves the time.
- Check whether the file is a universal load (
+page.js) or a server-only load (+page.server.js). Anything touching secrets or the filesystem belongs in the latter. - Read the full server-side stack trace in the terminal, not just the browser's 500 page: SvelteKit hides details in production for security.
- For
$prefix errors, confirm the value is an actual store with asubscribemethod and that you are inside a component, not a plain module. - Use
$env/static/privateand$env/dynamic/privatefor secrets so the compiler enforces the server-only boundary for you. - Reproduce with
vite build && vite preview; dev-mode behaviour differs meaningfully from the production build.
Tools worth reaching for
vite build && vite previewSvelteKit terminal logssvelte-check$env/static/private
Authoritative references
Primary documentation for this error, worth reading before applying any fix in production.
Related Svelte errors
- Svelte 5: `$:` is not allowed in runes mode, use `$derived` or `$effect` insteadAs soon as a component uses any rune ($state, $props, $derived), Svelte compiles the whole…
- Svelte 5: `$effect` can only be used inside an effectEffects have to be created while a component is initialising, so calling `$effect` from a…
- Svelte 5: `$props()` can only be used at the top level of componentsRunes are compiled away rather than called, so `$props()` has to appear exactly once as the…
- Svelte 5: `export let` is not allowed in runes mode, use `$props()` insteadA component is in runes mode, where props arrive through $props() rather than exported let…
- Svelte 5: $state / $derived can only be used inside a .svelte or .svelte.js fileRunes are compiler features, not runtime functions. They only work in files the Svelte…
- Svelte 5: Cannot export state from a module if it is reassignedIn a .svelte.js or .svelte.ts module, $state works because the compiler rewrites every read…
- Svelte 5: effect_update_depth_exceeded (Maximum update depth exceeded)An effect wrote to state that it also reads, so running it schedules it again and Svelte…
- Svelte 5: State referenced in its own scope will never updateReading a `$state` variable into a plain `const`, or passing it to a function, copies the…
Browse other categories
- HTTP 494xx client errors, 5xx server errors, redirects, headers and protocol problems.
- JavaScript 42npm resolution, async pitfalls, hydration, memory limits and runtime type…
- Database 41Connections, deadlocks, constraints, replication and memory limits.
- AI 35Rate limits, context windows, GPU memory and model-serving failures.
- Network 35Refused connections, timeouts, resets, MTU problems and port exhaustion.
- Python 35Imports, virtual environments, encoding, concurrency and dependency conflicts.
- Kubernetes 34CrashLoopBackOff, ImagePullBackOff, OOMKilled, RBAC, scheduling and storage.
- Docker 27Daemon connectivity, disk space, image pulls, ports and architecture mismatches.
- System 26Disk space, systemd units, file descriptors, OOM killer and scheduled jobs.
- Cloud 25IAM permissions, quotas, service limits and credential failures.
- Security 25JWT validation, CSRF, OAuth grants, SELinux, SSH host keys and CSP.
- TLS 24Untrusted authorities, expiry, hostname mismatch, chains and cipher negotiation.
Something missing or wrong?
This entry is maintained by hand. If the fix is out of date, incomplete, or you have a better one, email a correction and it will be reviewed.