- Published on
CORS Explained, Why Your Request Is Being Blocked
- Authors

- Name
- Alex Peng
- @aJinTonic
You hit a public API from your frontend app. The network tab shows a successful response. But the browser blocks it, and the console says:
Access to XMLHttpRequest at 'https://api.example.com/users'
from origin 'http://localhost:3000' has been blocked by CORS policy
This is not an API error. The server responded. The browser blocked you from reading the response. Here's why.
The Same-Origin Policy
Browsers enforce the same-origin policy: JavaScript can only read responses from the same origin as the page it's running on.
An origin is the combination of scheme + host + port:
| URL | Origin |
|---|---|
https://example.com/api/users | https://example.com |
https://example.com:8080/api | https://example.com:8080 |
http://example.com/api | http://example.com |
https://api.example.com | https://api.example.com |
Two URLs are same-origin only if all three parts match exactly. This is why localhost:3000 is a different origin from api.example.com.
The same-origin policy prevents a malicious page from making requests to https://yourbank.com and reading the response using your browser's cookies. It's a security feature, not a bureaucratic nuisance.
What CORS Is
Cross-Origin Resource Sharing (CORS) is a mechanism for servers to opt into cross-origin access. Without CORS, the same-origin policy blocks everything. With CORS, servers can say "requests from this other origin are allowed."
It works through HTTP headers.
Simple Requests
For requests that meet specific criteria (GET/HEAD/POST with certain content types), the browser just adds an Origin header to the request:
GET /api/users HTTP/1.1
Origin: http://localhost:3000
The server responds with an Access-Control-Allow-Origin header:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
If the header matches the request's origin (or is * for public APIs), the browser allows JavaScript to read the response. If it's missing or doesn't match, the browser blocks it.
Note: the request went through. The server processed it and responded. CORS only controls whether the browser lets your JavaScript read the response.
Preflight Requests
For non-simple requests, DELETE, PUT, POST with JSON body, requests with custom headers, the browser sends a preflight OPTIONS request first:
OPTIONS /api/users/42 HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Content-Type, Authorization
The server responds with what it allows:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Only if the preflight succeeds does the browser send the actual request. Access-Control-Max-Age tells the browser how long to cache the preflight result so it doesn't need to repeat it.
Credentials
By default, cross-origin requests don't include cookies or HTTP authentication. To include them, you need both sides to opt in:
Client:
fetch('https://api.example.com/users', {
credentials: 'include', // send cookies with the request
})
Server:
Access-Control-Allow-Origin: http://localhost:3000 // must be specific, not *
Access-Control-Allow-Credentials: true
When credentials are involved, Access-Control-Allow-Origin: * is not allowed. The server must specify the exact origin.
Fixing CORS Issues
The fix is always on the server, not the client. Common approaches:
Development: use a proxy in your dev server to avoid cross-origin requests entirely:
// vite.config.ts
export default {
server: {
proxy: {
'/api': 'http://localhost:8080',
},
},
}
Production: configure your server to send the right headers:
// Express
import cors from 'cors'
app.use(cors({
origin: ['https://yourapp.com', 'https://staging.yourapp.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}))
Don't use origin: '*' in production for authenticated APIs, it bypasses the credential restriction and is a security risk.
The Mental Model
CORS is not a bug. It's the browser asking the server: "This page wants to read your response, do you consent?" The server answers with headers. The browser enforces the answer. Your job as a developer is to configure your server to consent to the right origins, and nothing else.