CoreMesh

CORS Error? Which Header Is Missing and How to Fix It

Decode the browser's CORS error message, find the missing Access-Control header, handle preflight OPTIONS requests, and fix localhost setups safely.

CORS Error? Which Header Is Missing and How to Fix It

A CORS error means the browser sent your cross-origin request, received a response, and then refused to let your JavaScript read it because the server never said your origin was allowed. The fix is nearly always on the server. It has to send the right Access-Control-Allow-Origin header, and for anything beyond a plain GET or a form-style POST it also has to answer a preflight OPTIONS request with the methods and headers it accepts.

Here's the message Firefox prints in the console:

text
Cross-Origin Request Blocked: The Same Origin Policy disallows
reading the remote resource at https://some-url-here. (Reason:
additional information here).

The text after Reason: names the exact header that's missing or wrong, so read it before touching any code. Chrome words its message differently, but the console is still the only place the detail shows up. For security reasons your JavaScript never sees the specifics. All it knows is that an error occurred, which is why catch blocks can't tell a CORS failure from a dead server.

What a CORS error means#

An origin is the scheme, domain and port taken together. https://app.example and https://api.example are different origins, and so are http://localhost:3000 and http://localhost:8080. By default, fetch() and XMLHttpRequest follow the same-origin policy: a page may only read responses from the origin it was loaded from.

Cross-Origin Resource Sharing (CORS) is the HTTP-header mechanism that lets a server relax that rule. The browser adds an Origin request header. If the response carries an Access-Control-Allow-Origin header that covers that origin, the browser hands the response to your code. If it doesn't, the browser blocks the read and logs the error.

Two consequences trip people up:

  • The server usually did receive the request and often answered with a 200. There's no "CORS status code". The block happens inside the browser after the response arrives. Preflight failures are the exception, covered below.
  • A CORS error isn't always a bug. The remote site may be refusing cross-origin reads on purpose. If the endpoint is supposed to be reachable from your page, though, something in the server config is off.

The same rules apply to web fonts loaded through @font-face, WebGL textures and images drawn to a canvas, but the errors you'll meet day to day come from fetch().

Read the error message before changing anything#

Open the developer tools, reproduce the failing request, and look at the console. Firefox's reason strings map directly to a cause. The ones you'll see most:

Reason in the console What happened Fix it where
CORS header 'Access-Control-Allow-Origin' missing The response has no allow-origin header at all Server
CORS header 'Access-Control-Allow-Origin' does not match 'xyz' Header present, but it lists a different origin Server
Credential is not supported if the CORS header 'Access-Control-Allow-Origin' is '*' Request carried cookies or auth, server replied with the wildcard Server
expected 'true' in CORS header 'Access-Control-Allow-Credentials' Credentialed request, server didn't opt in Server
Did not find method in CORS header 'Access-Control-Allow-Methods' Preflight response didn't list your method Server
missing token 'xyz' in CORS header 'Access-Control-Allow-Headers' from CORS preflight channel Preflight response didn't list a header you send Server
CORS preflight channel did not succeed The OPTIONS request itself failed Server
CORS request did not succeed Request never completed: network, TLS or a down server Network or server
Multiple CORS header 'Access-Control-Allow-Origin' not allowed Two layers each added the header Server config
CORS request external redirect not allowed A preflighted request got redirected to another origin Server
CORS request not http The requested URL isn't an HTTP URL Local setup

Nearly every row says "server". That's the honest picture: most CORS errors can only be resolved on the server, because the server decides who may read its responses. The full list of reasons, each with its own page, is in MDN's CORS errors reference.

Which server header is missing#

Six response headers do all the work.

Header What it controls Typical value
Access-Control-Allow-Origin Which origin may read the response. Exactly one header, one origin or * https://app.example
Access-Control-Allow-Methods Methods allowed, sent in the preflight response GET, POST, PUT, DELETE
Access-Control-Allow-Headers Request headers allowed, sent in the preflight response Content-Type, Authorization
Access-Control-Allow-Credentials Whether cookies and HTTP auth may be sent true
Access-Control-Expose-Headers Response headers JavaScript is allowed to read X-Request-Id
Access-Control-Max-Age Seconds the browser may cache a preflight result 600

The smallest response that fixes a public, uncredentialed GET:

http
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/json

If you want to allow one origin instead of everyone, name it and add Vary: Origin so caches don't serve one origin's answer to another:

http
Access-Control-Allow-Origin: https://app.example
Vary: Origin

This script applies the same checks the browser applies to the actual response. Edit the headers at the bottom to match what your server sends and it prints the Firefox reason you'd get.

python
def cors_reason(origin, credentials, headers):
    """Return the Firefox 'Reason' text for a response, or 'OK'."""
    acao = [v for n, v in headers if n.lower() == "access-control-allow-origin"]
    acac = [v for n, v in headers if n.lower() == "access-control-allow-credentials"]

    if not acao:
        return "CORS header 'Access-Control-Allow-Origin' missing"
    if len(acao) > 1:
        return "Multiple CORS header 'Access-Control-Allow-Origin' not allowed"
    allowed = acao[0]
    if allowed == "*" and credentials:
        return ("Credential is not supported if the CORS header "
                "'Access-Control-Allow-Origin' is '*'")
    if allowed != "*" and allowed != origin:
        return f"CORS header 'Access-Control-Allow-Origin' does not match '{allowed}'"
    if credentials and acac != ["true"]:
        return "expected 'true' in CORS header 'Access-Control-Allow-Credentials'"
    return "OK"


page = "https://app.example"
cases = {
    "no header":        (False, [("Content-Type", "application/json")]),
    "wildcard":         (False, [("Access-Control-Allow-Origin", "*")]),
    "wildcard+cookies": (True,  [("Access-Control-Allow-Origin", "*")]),
    "wrong origin":     (False, [("Access-Control-Allow-Origin", "https://old.example")]),
    "origin, no creds": (True,  [("Access-Control-Allow-Origin", page)]),
    "origin + creds":   (True,  [("Access-Control-Allow-Origin", page),
                                 ("Access-Control-Allow-Credentials", "true")]),
    "doubled header":   (False, [("Access-Control-Allow-Origin", page),
                                 ("Access-Control-Allow-Origin", page)]),
}

for name, (creds, headers) in cases.items():
    print(f"{name:18} -> {cors_reason(page, creds, headers)}")

Preflight requests and why OPTIONS matters#

Before sending a request that could cause side effects on the server, the browser asks permission first. It sends an OPTIONS request to the same URL with headers describing the method and headers the real request will use. Only if the server answers with matching Access-Control-Allow-Methods and Access-Control-Allow-Headers does the real request go out. This is the preflight, and it's the step most hand-written servers forget.

A request skips the preflight only when all of these hold:

  • The method is GET, HEAD or POST.
  • The only headers you set by hand are Accept, Accept-Language, Content-Language, Content-Type or Range with a single range value.
  • Content-Type is application/x-www-form-urlencoded, multipart/form-data or text/plain.
  • No upload event listeners on an XMLHttpRequest, and no ReadableStream body.

That list explains the two preflights almost every app triggers: Content-Type: application/json and an Authorization header. Both are outside the safelist, so a JSON POST with a bearer token is preflighted every time.

Two rules about the preflight itself. It never carries credentials, so don't gate the OPTIONS handler behind auth or it'll fail with CORS preflight channel did not succeed. And if the real request will send cookies, the preflight response must include Access-Control-Allow-Credentials: true too.

Run this to see which of your requests will be preflighted and why:

javascript
const SAFE_METHODS = ["GET", "HEAD", "POST"];
const SAFE_HEADERS = ["accept", "accept-language", "content-language", "content-type", "range"];
const SAFE_TYPES = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"];

function preflightReasons({ method = "GET", headers = {} }) {
  const reasons = [];
  if (!SAFE_METHODS.includes(method.toUpperCase())) {
    reasons.push(`method ${method}`);
  }
  for (const [name, value] of Object.entries(headers)) {
    const lower = name.toLowerCase();
    if (!SAFE_HEADERS.includes(lower)) {
      reasons.push(`header ${name}`);
    } else if (lower === "content-type") {
      const type = value.split(";")[0].trim().toLowerCase();
      if (!SAFE_TYPES.includes(type)) reasons.push(`Content-Type ${type}`);
    }
  }
  return reasons;
}

const requests = {
  "plain GET": { method: "GET" },
  "form POST": { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" } },
  "JSON POST": { method: "POST", headers: { "Content-Type": "application/json" } },
  "GET + bearer": { method: "GET", headers: { Authorization: "Bearer abc" } },
  "DELETE": { method: "DELETE" },
  "PUT JSON + custom": { method: "PUT", headers: { "Content-Type": "application/json", "X-Trace-Id": "1" } },
};

for (const [label, req] of Object.entries(requests)) {
  const why = preflightReasons(req);
  console.log(`${label.padEnd(18)} ${why.length ? "PREFLIGHT  (" + why.join(", ") + ")" : "simple"}`);
}

How to fix a CORS error, in order#

Work down this list. Each step is safer and more common than the one after it.

1. Add Access-Control-Allow-Origin on the server. This fixes the "missing" and "does not match" reasons, which together cover most reports. Return * for public data, or the exact requesting origin plus Vary: Origin for anything else. Confirm: reload, and the console reason disappears for GET requests.

2. Answer OPTIONS requests. Respond with the allow-origin header plus Access-Control-Allow-Methods and Access-Control-Allow-Headers listing everything your client sends. Add Access-Control-Max-Age so the browser can cache the answer. Confirm: the "Did not find method" and "missing token" reasons are gone and your PUT, DELETE and JSON POST calls succeed.

A bare-bones Node server that does both:

javascript
import http from "node:http";

const ORIGIN = "https://app.example";

http.createServer((req, res) => {
  res.setHeader("Access-Control-Allow-Origin", ORIGIN);
  res.setHeader("Vary", "Origin");
  if (req.method === "OPTIONS") {
    res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
    res.setHeader("Access-Control-Max-Age", "600");
    res.writeHead(204).end();
    return;
  }
  res.setHeader("Content-Type", "application/json");
  res.end(JSON.stringify({ ok: true }));
}).listen(8080);

3. Fix credentialed requests. If the page sends cookies or HTTP auth, the server must return a specific origin, never *, and Access-Control-Allow-Credentials: true. The wildcard is also banned in Allow-Methods, Allow-Headers and Expose-Headers for these requests. Confirm: the "Credential is not supported" and "expected 'true'" reasons stop appearing.

4. Expose custom response headers. If response.headers.get("X-Request-Id") returns null even though the header is there, add Access-Control-Expose-Headers: X-Request-Id. Confirm: the value is readable.

5. Avoid the preflight from the client. If you can't change the server but it does set allow-origin, reshape the request so it's simple: GET, HEAD or POST only, safelisted headers only, a form or text content type. This sidesteps a whole class of preflight errors. Confirm: the preflight checker above reports "simple".

6. Use mode: "no-cors" when you don't need the response. Analytics beacons and cache warming from a service worker fit here.

javascript
fetch("https://api.example.com/log", {
  method: "POST",
  mode: "no-cors",
  body: data,
});

The response comes back opaque: status 0, empty headers, unreadable body. That's the trade. It won't help you load data.

7. Proxy through a server you control. When a third-party API sets no CORS headers at all, have your own backend fetch it and return the result with your headers. It adds latency and another moving part, but it's the only correct option when you don't own the remote server. Confirm: your page calls your own origin and the console is clean.

8. Browser extensions and security flags. Treat these as a last resort for a quick local experiment, and understand the risk: they switch off a protection for every site in that browser profile, and they change nothing for your users. A "CORS unblock" extension on your machine means the bug ships. Use a throwaway profile if you must, and never run a disabled-security browser for normal browsing.

CORS errors on localhost#

Local setups produce CORS errors for one simple reason: the port is part of the origin. A front end on http://localhost:3000 calling an API on http://localhost:8080 is cross-origin, exactly as if they were on different domains.

The fixes, in the order I'd try them:

  1. Allow the dev origin on the API. Return Access-Control-Allow-Origin: http://localhost:3000 from the local API, with the preflight handling from step 2 above. This matches production behavior most closely, so bugs show up early.
  2. Proxy the API through the dev server. Most front-end dev servers can forward API paths to another port so the browser sees a single origin. That's the "proxy through a server you control" fix applied to development. Check your dev server's docs for the option name.
  3. Serve the page over HTTP, not from disk. A page opened straight from the filesystem resolves relative URLs to file://, and fetching those gives the CORS request not http reason. Any local static server fixes it.

One thing not to do: don't set * on the API because it's "just dev" and then forget it. If the production app sends cookies, * is exactly the config that breaks.

Common mistakes and gotchas#

  • Fixing the GET but not the OPTIONS. The allow-origin header on the real response does nothing if the preflight already failed. Both responses need it.
  • Wildcard plus credentials. Access-Control-Allow-Origin: * with cookies is blocked, and a Set-Cookie in a wildcard response won't set anything either.
  • Two layers setting the header. The app adds it, then nginx or a CDN adds it again, and the browser rejects the doubled header. Pick one layer.
  • Reading the reason in JavaScript. You can't. The error carries no detail; only the console does.
  • Redirects after a preflight. Not every browser follows a redirect once a request has been preflighted. If the API redirects /v1/users to /v1/users/, hit the final URL directly, or make a simple request first and read Response.url to discover it. Requests that need Authorization can't use that trick.
  • Forgetting Vary: Origin. Reflecting the requesting origin without it lets a shared cache hand one origin's response to another.
  • Third-party cookie settings. Even with everything configured, a browser set to reject third-party cookies won't send them cross-origin. CORS can't override that.
  • Safari and odd Accept values. WebKit treats non-standard values in Accept, Accept-Language and Content-Language as non-simple and preflights the request. Other browsers don't.

Quick reference#

Walk through this before posting the error anywhere:

  • [ ] Open the console and copy the Reason: text. Match it to the table above.
  • [ ] Confirm the two origins really differ. Scheme, host and port all count.
  • [ ] Check the actual response for exactly one Access-Control-Allow-Origin, and Vary: Origin if it's not *.
  • [ ] Check whether the request is preflighted: a non-simple method, application/json, or Authorization all mean yes.
  • [ ] If preflighted, check the OPTIONS response lists your method in Allow-Methods and every custom header in Allow-Headers.
  • [ ] If cookies or auth are sent: explicit origin, no wildcards anywhere, Access-Control-Allow-Credentials: true.
  • [ ] Need a custom response header in JS? Add it to Access-Control-Expose-Headers.
  • [ ] Don't own the server? Proxy it, go no-cors if you don't need the body, or make the request simple.
  • [ ] Never ship a fix that only exists in your browser.

Copy-paste starting point for a private API:

http
Access-Control-Allow-Origin: https://app.example
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Expose-Headers: X-Request-Id
Access-Control-Max-Age: 600
Vary: Origin

The background on every header is in MDN's CORS guide, and the mode option is documented under fetch().

Quick answers

What is a CORS error?

It's the browser refusing to let your script read a response from another origin because the server didn't include an Access-Control-Allow-Origin header that covers your page's origin. The request usually reached the server; the block happens in the browser afterward.

What status code does a CORS error have?

None. The server may have returned 200 and the browser still blocks the read. Your JavaScript only learns that an error occurred, and the real reason is printed in the devtools console, not in a status code.

How do I fix a CORS error in React?

The React dev server and your API run on different ports, so they're different origins. Either make the API return Access-Control-Allow-Origin for the dev origin and answer OPTIONS preflights, or route API calls through the dev server's proxy so the browser only sees one origin.

Can I bypass a CORS error in Chrome with an extension?

You can, but it only disables the check in your own browser profile, which is risky for every other site you visit and does nothing for your users. Fix the server headers, proxy the request through a server you own, or use no-cors mode if you don't need to read the response.

Why does a CORS error happen on localhost?

The port is part of the origin, so localhost:3000 talking to localhost:8080 is a cross-origin request. Allow the dev origin on the API or proxy through the dev server, and serve the page over HTTP rather than opening it from disk.

Did this solve it?

One tap. No account.

Saved you an hour? Send a tip.

USDC or ETH on Base, Ethereum, Arbitrum and more. Keeps the notes free and the examples running.

Base

Network

0xc783bc5c3fa14cEC3f09A22284bC91C5AF0d59e1
Open in wallet · 5 USDC

On a computer? Scan the code with your phone wallet, or copy the address.

Pick the network your wallet or exchange uses. USDC works on all six; ETH on Base, Ethereum, Arbitrum and Optimism. Don't send on Tron or Solana: this address doesn't exist there. Tips are optional and unlock nothing; every guide stays free.

Discussion

Start the discussion

`code` and ``` blocks work

No account needed. Comments are read before they show up, usually the same day. Your email is never asked for.

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS