What is it? #
CORS is a browser rule. A page on one origin cannot read a response from a different origin unless that server explicitly allows it.
It is not a server security feature. It protects users from one site reading another site's data using their credentials, and it does nothing against a direct request from curl or a server.
That distinction matters, because "I disabled CORS to fix it" usually means allowing any origin to read authenticated responses — which is the thing CORS exists to prevent.
The rules are unintuitive largely because of preflight requests: for anything beyond a simple request, the browser asks permission before sending.
Think of it like this #
A receptionist who will pass a message to a visitor only if the department has listed that visitor's company in advance.
The rule protects the department's clients, not the building. Someone phoning directly from outside is not affected at all.
Simple example #
A frontend on app.example.com calls an API on api.example.com. The browser blocks it until the API responds with headers naming that origin as allowed.
Code #
Simple request — sent immediately, response blocked if not allowed
GET /orders
Origin: https://app.example.com
Access-Control-Allow-Origin: https://app.example.com
→ the browser lets the page read the response
Preflight — an OPTIONS request first, before the real one
OPTIONS /orders
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
→ only then is the POST sent
What triggers a preflight
any method other than GET, HEAD or POST
Content-Type other than form-encoded, multipart or text/plain
→ application/json always triggers one
any custom header, including Authorization
Most real API calls trigger a preflight. Access-Control-Max-Age caches
the permission so it is not repeated for every request.
# Correct configuration: specific origins, not a wildcard
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://app.example.com",
"https://admin.example.com",
],
allow_credentials=True, # cookies or Authorization headers
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
max_age=86400,
)
# NEVER: allow_origins=["*"] with allow_credentials=True
# The specification forbids it, and browsers reject the combination —
# because it would let any site read authenticated responses.
Reading the error
"No 'Access-Control-Allow-Origin' header is present"
the server did not allow this origin — or returned an error before
the CORS middleware ran
"The value of 'Access-Control-Allow-Origin' must not be '*' when
credentials mode is 'include'"
exactly the unsafe combination described above
"Method PUT is not allowed by Access-Control-Allow-Methods"
the preflight response did not list that method
"Request header authorization is not allowed"
the preflight response did not list that header
CORS is not a substitute for authorisation
Allowing an origin lets a browser page READ the response. It says
nothing about who may access the data. A public API with permissive
CORS and no authentication is open to everyone either way.
Authorisation is enforced server-side, per request, regardless of origin.
How it works #
The browser attaches an Origin header to cross-origin requests and inspects the response headers before letting the page read it. The request usually reaches the server either way — CORS controls reading the response, not sending the request.
That is why a cross-origin POST can have side effects even when the response is blocked, and why CSRF protection is a separate concern.
Preflight exists so that a request with side effects is not sent at all unless the server has agreed to the method and headers. The browser asks first with OPTIONS.
Since application/json and Authorization both trigger preflight, essentially every authenticated API call performs one. Access-Control-Max-Age caches the answer, avoiding a doubled request count.
The wildcard-with-credentials prohibition is the important rule. Allowing any origin while permitting credentials would let any website make authenticated requests on a logged-in user's behalf and read the results. Browsers reject the combination outright.
CORS errors originating from a server error are a common confusion: if the application throws before the CORS middleware adds headers, the browser reports a CORS failure while the real problem is a 500.
The final point is the one most worth internalising. CORS is a browser-enforced rule about reading responses, and it is not authorisation.
Real-world use #
Nearly every separated frontend and backend hits CORS on the first day. The correct fix is listing the frontend origins; the common wrong fix is a wildcard.
Credentials mode matters. Cookie-based sessions across origins need allow_credentials plus SameSite cookie settings that permit cross-site sending, which is increasingly restricted by browsers.
Behind a CDN, Vary: Origin is necessary when the allowed origin varies, or a cached response for one origin can be served to another.
Development often uses a proxy so the frontend and backend share an origin, which removes CORS locally and means it is discovered in staging instead.
The reliable configuration is a short explicit list of origins, the methods and headers actually used, and credentials enabled only when needed.
Common mistakes #
- Using a wildcard origin with credentials, which browsers reject and which would be unsafe.
- Treating CORS as authorisation rather than as a browser reading rule.
- Not handling OPTIONS, so every preflight fails.
- Missing Vary: Origin behind a cache, leaking one origin’s response to another.
- Diagnosing a CORS error when the real problem is a server error before the middleware.
Practice #
Configure CORS for an API with two specific frontend origins, credentials enabled, and only the methods and headers actually used. Then make a cross-origin JSON request and observe the preflight in the browser network tab, including how Max-Age affects repeat requests.