What is CORS? Why a fetch works locally and fails once published

CORS is the browser's rule that a page at one origin may only read responses from another origin if that origin says so in a header. A form or script that worked on your machine and fails at an address, with a red line in the console, is almost always this.

What is CORS?

The markup. The highlighted line is the part this term is about.
The markup. The highlighted line is the part this term is about.

It is the rule browsers enforce on requests from a page at one origin to a server at another: the browser sends the request, and it will only let the page read the answer if the server's response carries an Access-Control-Allow-Origin header naming the page's origin.

The same request from a terminal works, because the rule is the browser's, not the server's, which is why a form that "works locally" fails the moment the page is published somewhere.

This guide covers what an origin is, why the rule exists, the exact console message, and the fix on the server side.

Access to fetch at 'https://api.example.com/data' from origin
'https://mysite.com' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.

That message is precise and reads like an accusation against your page. It is a statement about the server you called.

What an origin is, and why CORS cares

https://example.com          ← origin
https://example.com:443      ← same (443 is the default for https)
http://example.com           ← DIFFERENT (scheme)
https://api.example.com      ← DIFFERENT (host)
https://example.com:8080     ← DIFFERENT (port)
CORS is the browser's rule that a page may only read responses from another origin if that origin says so in a header. A form or script that worked on your machine and fails at an address is almost always this.
CORS is the browser's rule that a page may only read responses from another origin if that origin says so in a header. A form or script that worked on your machine and fails at an address is almost always this.

Scheme, host, port. All three must match. api.example.com is a different origin from example.com, which surprises people who assume a shared domain is enough.

The page you are reading Sandboxed frame the pasted HTML runs here it cannot reach anything outside this box
The browser is the wall. The page inside it can ask any server anything; it can read the answer only from servers that allow it.

Why the rule exists

Without it, any page you visited could read any site you were signed into — your mail, your bank — using your own cookies, and send the contents anywhere. Origin separation is what stops that, and CORS is the controlled exception to it.

Which side has to change

The server being called. It must send:

Access-Control-Allow-Origin: https://mysite.com

or, for a genuinely public endpoint:

Access-Control-Allow-Origin: *

Nothing you do in the page can grant this. Not a header on your side, not a fetch option, not a proxy trick in the browser. If you do not control the server, your options are to ask them, use a server of your own as an intermediary, or use a different endpoint.

The confusing part

<script>
  fetch('https://api.example.com/save', { method: 'POST', body: data });
  // console shows a CORS error — and the server saved the data
</script>

For simple requests the browser sends the request and then blocks your script from reading the response. So the server has already acted. That is why a form submission can appear to fail and turn out to have worked.

Preflight

Non-simple requests — anything with Content-Type: application/json, a custom header, or a method other than GET, POST or HEAD — trigger a preflight first:

OPTIONS /save
Origin: https://mysite.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

The server must answer with:

Access-Control-Allow-Origin: https://mysite.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: content-type
Access-Control-Max-Age: 86400

Here the block happens before the real request, so nothing reaches the server at all. Max-Age lets the browser cache the answer instead of asking before every call.

Credentials

<script>
  fetch(url, { credentials: 'include' });
</script>

To send cookies the server must reply with Access-Control-Allow-Credentials: true and name a specific origin. A wildcard is rejected when credentials are involved — deliberately, since "any site may read this with the user's cookies" is never a safe thing to say.

Why it appears at publishing time

Locally your page and your API are often both on localhost, which is one origin, so nothing is blocked. Published, they are on different hosts and the rule engages.

This is the single most common reason a form that worked locally fails once published. Test against the real origins before you rely on it.

Not CORS

Two adjacent errors that are not CORS:

  • Mixed content — an http request from an https page. Blocked for a different reason, fixed by using https.
  • Content-Security-Policy — your own page's policy forbidding the connection. The message names the policy, and you fix it on your side.

Reading the error correctly

The message names which part failed, and each points somewhere different.

Message fragment What happened Who fixes it
No 'Access-Control-Allow-Origin' header The server did not permit your origin The server
does not match the supplied origin It permitted a different origin The server
Response to preflight request doesn't pass The OPTIONS answer was wrong or missing The server
credentials mode is 'include' with a wildcard Wildcard plus cookies is not allowed The server, naming your origin
has been blocked by CSP Not CORS — your own page's policy You — see Content-Security-Policy
Mixed Content Not CORS — an http request from https You — see mixed content

The last two rows are the ones that waste time, because they appear in the same console position as a CORS failure and are fixed on the opposite side.

Testing before you publish

Run your page from a local server rather than opening the file, and point it at the real endpoint. Opening the file gives you the file:// origin, which behaves differently again — see the file protocol.

python3 -m http.server 8000

That gives the page a real origin, so a CORS problem shows up now rather than after deployment.

Three things that are not CORS

A request that never happens. If the console shows no CORS error and the Network panel shows no request, the script failed before the fetch; read the first console error instead.

A 404 or 500. The server answered with an error, and the answer may lack the header, so the browser reports CORS as well. Fix the server error first; the CORS message is a symptom.

A file:// page. A page opened from disk has no origin the server can name, so every cross-origin request fails. Serve the page at an address and test there.

Allowing everyone, and when that is fine

Access-Control-Allow-Origin: * lets any page read the response. That is correct for a public, read-only resource, a published data file or an image, and wrong for anything that identifies the reader or accepts a submission from them. For a form endpoint, name the origins that may post to it and no others.

Fixing a CORS error: 4 steps

  1. Read the console error. It names the origin that was refused and the address that refused it. That is the whole diagnosis.
  2. Add the header on the server. Access-Control-Allow-Origin with the page's origin, plus Allow-Methods and Allow-Headers for what the request uses.
  3. Answer the preflight. A POST with JSON triggers an OPTIONS request first; the server must answer it with the same headers and a 200.
  4. If you do not control the server, use a receiver that does. A form service, or your own small endpoint that allows your origin. Sharing a form covers the options.

Questions people ask

What is an origin?

The combination of scheme, host and port. Any difference in any of the three — http versus https, a different subdomain, a different port — makes it a different origin.

Who fixes a CORS error?

The server being called. It has to send an Access-Control-Allow-Origin header naming your page's origin. Nothing you change in the page can grant that.

Why did the request go through if it was blocked?

Because CORS blocks your script from reading the response, not the server from receiving it. For simple requests, the server has already acted.

What is a preflight?

For non-simple requests the browser first sends an OPTIONS request asking what is permitted. If that is not answered correctly, the real request never happens.

Keep reading