CORS
Date: 2026-08-16
The mechanism by which a server permits other origins to read its responses. Every “blocked by CORS policy” error is the server declining, not your request being malformed — which is why fixing it in the front end never works.
What it is
CORS — Cross-Origin Resource Sharing — is a set of HTTP headers a server sends to tell the browser which origins may read its responses, relaxing the same-origin policy in a controlled way.
The enforcement is entirely in the browser. A server without CORS headers is not protecting itself — curl, Postman and any backend can read it freely. CORS protects users from having their authenticated data read by sites they didn’t authorise.
The simple case
BROWSER SERVER (api.example.com)
fetch('https://api.example.com/data') ─▶
Origin: https://shop.example.com
◀─ 200 OK
Access-Control-Allow-Origin:
https://shop.example.com
browser checks the header against the page's origin
match → JavaScript receives the response
no match → JavaScript receives an error. The response was fetched
and then discarded.
The request was sent and the server did the work. The browser threw the response away before your code saw it. This is why a failing CORS request can still create a record on the server.
Preflight
Requests that could change state get checked before being sent. The browser issues an OPTIONS request first:
OPTIONS /data ─▶
Origin: https://shop.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization
◀─ 204
Access-Control-Allow-Origin: https://shop.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 86400
then the real PUT is sent
A request avoids preflight only if it’s “simple”: GET, HEAD or POST, with no custom headers, and a Content-Type of application/x-www-form-urlencoded, multipart/form-data or text/plain.
That last condition catches everyone — Content-Type: application/json triggers a preflight. Nearly every real API call is therefore two round trips, not one, which is a latency cost worth knowing about. Access-Control-Max-Age lets the browser cache the preflight result and skip it for subsequent requests.
Credentials
Cookies and authorisation headers are not sent cross-origin by default:
fetch(url, { credentials: 'include' }) // ask for cookies to be sentThe server must then respond with:
Access-Control-Allow-Origin: https://shop.example.com ← must be exact
Access-Control-Allow-Credentials: true
With credentials, the wildcard is forbidden. Access-Control-Allow-Origin: * is rejected outright when credentials are involved — the server has to name the origin. That’s deliberate: it forces a conscious decision about who may read authenticated data.
The cookie also needs SameSite=None; Secure to be sent cross-site at all — see Cookies.
Reading the errors
| Message | Means |
|---|---|
| ”No ‘Access-Control-Allow-Origin’ header” | Server sent no CORS headers. Fix the server |
| ”…does not match” | Server named a different origin — often it echoed the wrong one, or you’re on www. and it allows the apex |
| ”…wildcard ’*’ when credentials mode is ‘include‘“ | Server must name your origin explicitly |
| ”Method PUT is not allowed” | Preflight response omits it from Allow-Methods |
| ”Request header field x-foo is not allowed” | Preflight response omits it from Allow-Headers |
| Preflight returns 404 or 405 | Server doesn’t handle OPTIONS at all — very common on hand-rolled routers |
In plain terms: every one of these is the server’s answer, not your request’s fault. No amount of changing the fetch call fixes a missing header — you need the server, or a proxy, or the API’s owner.
Practical
- You cannot fix CORS from the front end. If you don’t control the API, proxy it through your own origin — which also removes the preflight and keeps any secret server-side
- Never reflect the
Originheader unconditionally intoAccess-Control-Allow-Originwhile allowing credentials. That’s a wildcard with extra steps, and it permits any site to read authenticated responses - Set
Access-Control-Max-Ageso preflights are cached. On a chatty API this halves request count - CORS is not authorisation. It controls which origins may read a response, not which users may. Authentication and permission checks still belong on the server
- Beware
Vary: Originon cached responses — a CDN caching one origin’s allow header and serving it to another produces failures that only appear in production, and only sometimes