Issue #1052
You call an API from your web page and the browser refuses to hand you the response, even though the server replied with a 200. The console shows a message about Access-Control-Allow-Origin. The request left your machine, reached the server, and came back, yet your code never sees the body. CORS is the mechanism deciding that, and it lives entirely in the browser.
The same-origin policy
By default a browser lets a page read responses only from its own origin. An origin is the combination of scheme, host, and port, so https://app.example.com and https://api.example.com are different origins, and so are http and https versions of the same host. This rule, the same-origin policy, stops a random site you visit from quietly reading your bank’s API using cookies already in your browser.
CORS, Cross-Origin Resource Sharing, is how a server says it is fine for a different origin to read its responses. The server opts in with response headers, and the browser enforces the result.
The server opts in with a header
When your page makes a cross-origin request, the browser attaches an Origin header naming where the request came from.
GET /items HTTP/1.1
Origin: https://app.example.com
The server answers, and if it wants to allow that origin, it echoes it back.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Content-Type: application/json
The browser compares the Access-Control-Allow-Origin value against the page’s origin. If they match, your fetch resolves with the body. If the header is missing or names a different origin, the browser throws away the response and your code gets an error. The data crossed the wire either way, the browser just declines to expose it.
A server can also reply with Access-Control-Allow-Origin: * to allow any origin. That is fine for public, non-credentialed APIs, but it does not work once cookies enter the picture.
What a CORS error looks like
When the header is missing, the browser logs something like this and rejects the response.
GET https://api.example.com/items net::ERR_FAILED 200 (OK)
Access to fetch at 'https://api.example.com/items' from origin
'https://app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
The 200 (OK) next to ERR_FAILED is the tell. The server handled the request and replied successfully, so this is not a server failure. The browser fetched the response, found no allow header, and refused to pass it to your code. You fix it on the server by sending the header, not by changing the client.
Simple requests and preflight
Some requests go straight through with just the Origin header. The spec calls these simple requests, and a request qualifies only when all of the following hold.
The method is GET, HEAD, or POST. The only headers your code sets by hand are ones on the safelist, such as Accept, Accept-Language, or Content-Type. And the Content-Type, if present, is one of application/x-www-form-urlencoded, multipart/form-data, or text/plain. A plain form post fits this, so does a basic GET.
// Simple request, sent directly with an Origin header
fetch('https://api.example.com/items')
The moment you step outside that set, the request stops being simple and the browser preflights it. Sending JSON makes Content-Type: application/json, which is not on the list, so even a humble POST of JSON triggers a preflight. So does any custom header.
// Not simple, the browser sends an OPTIONS preflight first
fetch('https://api.example.com/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer token',
},
body: JSON.stringify({ name: 'First' }),
})
For a preflight the browser sends an OPTIONS request asking for permission before the real call.
OPTIONS /items HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
The server replies describing what it allows.
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Only if the preflight passes does the browser send the actual POST. This is why the preflight and the real response are two separate exchanges, and both need the Access-Control-Allow-Origin header.
When cookies are involved
To send cookies or other credentials on a cross-origin request, you set a flag on the client.
fetch('https://api.example.com/items', {
credentials: 'include',
})
Now the server has to answer with Access-Control-Allow-Credentials: true, and it can no longer use the wildcard. Access-Control-Allow-Origin must name the exact origin. Since the value now varies per caller, the server should also send Vary: Origin so caches do not serve one origin’s allow header to another.
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin
The detail that trips people up is that CORS is a browser feature, not a server protection. A server still receives and processes the request whatever the headers say, and tools like curl or a backend service ignore CORS completely. The headers only control whether a browser lets page scripts read the response.
Start the conversation