CORS (Cross-Origin Resource Sharing) is the standard mechanism by which a server explicitly permits JavaScript on one origin to read resources from another. To understand it you first need the Same-Origin Policy.
The same-origin policy
An origin is the combination of scheme + host + port. https://app.example.com and https://api.example.com differ in host, so they're different origins; http:// vs https:// differ, and a different port differs too. For security, browsers block a script from reading a response whose origin differs from the script's own. That's the same-origin policy, and it's a core defense against attacks that abuse the user's cookies.
CORS doesn't punch a hole in that policy so much as let the server declare, via response headers, that a given origin is allowed — opening cross-origin communication safely. Crucial point: CORS is enforced by the browser. It does not apply to curl or server-to-server calls.
Simple vs preflighted requests
The browser splits cross-origin requests into two kinds. A simple request uses GET/HEAD/POST with only a safe, restricted set of headers (for example a Content-Type of text/plain, application/x-www-form-urlencoded, or multipart/form-data). The browser sends it immediately, then checks the response's Access-Control-Allow-Origin header to decide whether the script may read it.
Anything else — say, Content-Type: application/json, a PUT/DELETE, or custom headers — triggers a preflight. Before sending the real request, the browser first sends an OPTIONS request asking the server 'am I allowed to send this?'
OPTIONS /v1/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
HTTP/1.1 204 No Content
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
If the server answers the preflight with the right allow headers, the browser then sends the actual PUT. Access-Control-Max-Age tells the browser how long to cache that permission, avoiding a repeated preflight on every request.
Credentials
To include credentials such as cookies or an Authorization header on a cross-origin request, both sides must opt in. The client requests with fetch(url, { credentials: 'include' }), and the server must return Access-Control-Allow-Credentials: true. A key constraint: when credentials are allowed, Access-Control-Allow-Origin cannot be the wildcard * — it must name a specific origin.
Errors you'll actually hit
- No
Access-Control-Allow-Originheader → The server isn't sending CORS headers at all. Fix the server; you cannot fix this from the frontend. - **Wildcard
*with credentials** → Withcredentials: 'include', a server sending*gets blocked. Reflect the specific origin instead. - Preflight returns 4xx/5xx instead of 2xx → Often an auth middleware intercepts
OPTIONSand returns 401. LetOPTIONSthrough without authentication. - It looked like CORS but was really a network/HTTPS failure → Read the exact browser console message.
- Misconception: CORS protects the user's browser, not your server. Server access control still needs real authentication and authorization.