What CORS actually does
By default, a browser will not let JavaScript loaded from origin A read responses from origin B. This is the same-origin policy. CORS is the standard way for origin B to say "actually, A is allowed to read me." The check happens entirely in the browser. Your server can't "fix CORS for everyone" — it can only opt in.
Three consequences follow:
- CORS only affects browser-based clients.
curl, mobile apps, server-to-server: no CORS. - If the response reaches the browser at all, your server did the work. CORS rejects happen after the request is sent.
- "Disable CORS" doesn't exist on the server side. You can only widen what you allow.
Simple vs preflighted requests
The browser splits requests into two categories. A "simple" request — GET/HEAD/POST with a few standard content types and no custom headers — is sent directly, and CORS is checked on the response. A "preflighted" request triggers an OPTIONS call first with Access-Control-Request-Method and Access-Control-Request-Headers; the browser only sends the actual request if the preflight passes.
Things that trigger preflight:
- Methods other than GET/HEAD/POST.
Content-Typeoutsideapplication/x-www-form-urlencoded,multipart/form-data, ortext/plain. This catches everyapplication/jsonPOST.- Custom headers (
Authorization,X-Anything).
In practice: any modern fetch with a JSON body or an auth header preflights.
The five errors that cover 95% of cases
1. "No 'Access-Control-Allow-Origin' header is present"
Your server didn't reply with Access-Control-Allow-Origin at all. Either the request didn't reach your CORS middleware, or you only respond on the success path and forgot the error path (4xx/5xx responses still need CORS headers).
2. "The value of Allow-Origin must not be the wildcard '*' when credentials are 'include'"
If your fetch uses credentials: 'include' (or your axios sets withCredentials: true), the server cannot reply with Access-Control-Allow-Origin: *. It must echo the specific origin and also send Access-Control-Allow-Credentials: true.
3. "Method/Header X is not allowed by Access-Control-Allow-Methods/Headers"
The preflight succeeded but didn't list the method or header you're using. Make sure your CORS config includes the actual headers (case-insensitive) — including Authorization, Content-Type, and any custom ones.
4. "Response to preflight has invalid HTTP status code 4xx"
Your OPTIONS handler returned an error. Common causes: auth middleware rejecting unauthenticated OPTIONS (preflight requests don't carry the credentials yet — they're a separate exchange), or a router that doesn't have an OPTIONS handler registered.
5. "Request header field X is not allowed"
The preflight asked for header X, server replied without listing X in Access-Control-Allow-Headers. Fix the server-side allowlist.
The debugging order
- Reproduce with curl. If curl succeeds, the request itself is fine — the issue is purely CORS configuration. If curl fails, fix that first; CORS is a red herring.
- Open DevTools → Network → the failing request. Look for two lines: the OPTIONS preflight (if any) and the actual request. Both have their own status and headers.
- Check the OPTIONS response. It must return 2xx and include
Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers. - Check the main response. Even on a real GET/POST, it must include
Access-Control-Allow-Origin. - If using credentials: server must echo the exact origin (not wildcard) and send
Access-Control-Allow-Credentials: true; client must sendcredentials: 'include'.
What not to do
- Don't set
Access-Control-Allow-Origin: *in production unless your API is genuinely public. Echo the request's origin against a server-side allowlist. - Don't proxy through your own backend just to dodge CORS for first-party APIs you control. Fix the config instead. (Proxy is fine when you can't change the upstream.)
- Don't install browser extensions that disable CORS. They paper over real bugs that will bite users.