How to Fix CORS Errors When Submitting a Form
Getting 'blocked by CORS policy' when your form posts with fetch()? What the error means, why it happens, and how to fix it — including the no-JS escape hatch.
The ShipMyForm team
· 4 min read
You wired your form up with fetch(), hit submit, and the console throws:
Access to fetch at 'https://…/f/YOUR_FORM_ID' from origin 'https://yoursite.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is
present on the requested resource.Your request probably did reach the server — the browser just refused to let your JavaScript read the response. This guide explains what that means, why it happens when a form posts to a different origin, and the handful of ways to fix it (including the one that sidesteps CORS entirely).
What CORS actually is
CORS (Cross-Origin Resource Sharing) is a browser rule: when your JavaScript
fetch()es a URL on a different origin than the page, the browser only hands
you the response if that server opts in with an Access-Control-Allow-Origin
header naming your origin. Your form's action lives on shipmyform.com; your
page lives on yoursite.com — different origin — so the check applies.
It's enforced by the browser, not the server. That's why the same request works
fine from curl or your backend but fails in the browser.
The escape hatch: a plain form POST has no CORS
Here's the part people miss. CORS only applies to requests your script reads. A normal HTML form submit is a navigation — the browser posts and follows the response — so CORS never applies to it:
<form action="https://shipmyform.com/f/YOUR_FORM_ID" method="POST">
<input name="email" type="email" required />
<textarea name="message" required></textarea>
<button type="submit">Send</button>
</form>This works cross-origin, today, with zero configuration. If you don't strictly
need an inline success state, this is the simplest "fix." You only meet CORS when
you switch to fetch() to avoid a page reload — which is worth it for the UX, so
here's how to get it right.
The real fix for fetch(): allow your origin
When you fetch() cross-origin, the server has to return your origin in
Access-Control-Allow-Origin. A form backend decides that from its
allowed-origins list. If your site isn't on it, the endpoint omits the header
and the browser blocks the read — the exact error above.
With ShipMyForm: open the form's Settings → Allowed domains and add your
site (e.g. yoursite.com). The endpoint then reflects your origin back in the
CORS header and the fetch succeeds. Adding yoursite.com also covers its
subdomains like www.yoursite.com.
const res = await fetch("https://shipmyform.com/f/YOUR_FORM_ID", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const data = await res.json(); // now readable: { ok: true } or a 422 with fieldsWith no allowed domains configured, the endpoint returns
Access-Control-Allow-Origin: * (any site can post) — convenient but
not recommended for a real form. Once you list your domains, it
reflects only those origins back, and a POST from anywhere else is rejected with
a 403. So the fix is to list your site, not to open it up.
Preflights and JSON bodies
For some requests the browser sends a preflight — an OPTIONS request that
asks permission before the real one. It's triggered by "non-simple" requests,
most commonly a Content-Type: application/json body:
| Body | Content-Type | Preflight? |
|---|---|---|
new FormData(form) | multipart/form-data | No (simple request) |
| URL-encoded string | application/x-www-form-urlencoded | No |
JSON.stringify(...) | application/json | Yes — an OPTIONS first |
Accept: application/json is itself safelisted, so it never forces a preflight —
only the JSON content type does. ShipMyForm answers the OPTIONS preflight
automatically (with the same allowed-origin logic), so JSON works too — but if
you want to avoid the extra round-trip, send a FormData body. It's also
simpler and handles file inputs.
Common mistakes that cause (or fake) CORS errors
- Origin not in the allowed list — the number-one cause. Add your exact
domain. Watch
httpvshttpsandwwwvs apex. - Reaching for
mode: 'no-cors'— this doesn't fix anything. It returns an opaque response:res.okis always false-ish,res.json()throws, and you can't read{ ok: true }or validation errors. Never use it here. credentials: 'include'— a public form endpoint uses no cookies, so don't set this. Combined with a wildcard origin it actually causes a CORS failure.- Wrong URL — a typo'd form ID or an
http://endpoint on anhttps://page (mixed content) surfaces as a network/CORS-looking error. - Assuming the POST failed — a CORS block happens after a successful request in many cases; your submission may already be stored. Fix the header, don't resubmit blindly.
Quick checklist
- Submitting with
fetch()? Add your site to the form's allowed domains. - Prefer a
FormDatabody to skip the preflight. - Never use
mode: 'no-cors'orcredentials: 'include'. - Just want it working with no JS? Use a plain
<form action>— no CORS at all.
Next steps
- The full pattern: submit a form with fetch().
- Doing it in a component? Next.js form handling.
- Start free — 100 submissions a month, no card required.
Frequently asked questions
- Why does my form submission get a CORS error?
- The browser blocks a cross-origin fetch() when the server's response doesn't include an Access-Control-Allow-Origin header matching your site. With a form backend, that usually means your site's domain isn't on the form's allowed-origins list, so the endpoint won't return your origin in that header.
- Do plain HTML forms have CORS problems?
- No. A normal <form method="POST" action="…"> submit is a navigation, not a fetch your JavaScript reads, so the browser doesn't apply CORS to it. It works cross-origin regardless. CORS only comes into play when you submit with fetch() or XMLHttpRequest and read the response.
- Should I use mode: 'no-cors' to fix it?
- No. mode: 'no-cors' doesn't grant access — it makes the response opaque, so you can't read the JSON (result.ok, validation fields, etc.) and can't tell success from failure. Fix the allowed origins instead, or use a plain form POST.
- Does sending JSON trigger a CORS preflight?
- Yes. A Content-Type of application/json isn't CORS-safelisted, so the browser sends a preflight OPTIONS request first. Sending a FormData body instead keeps it a simple request with no preflight. Either works with ShipMyForm as long as your origin is allowed — it answers the preflight automatically.
Related guides
How to Submit a Form with JavaScript fetch()
The vanilla-JS pattern for submitting a form without a page reload — intercept submit, fetch() the data, and render success and per-field errors from the JSON response.
How to Handle Form Submissions in Next.js
Three ways to handle a Next.js form — Server Actions, Route Handlers, or a form backend — with code for each and a clear rule for choosing.
What Is a Form Backend? (And When You Need One)
The plain-language definition, how form endpoints work, and when a hosted backend beats rolling your own.