
A valid fetch call may return a 200 OK status on the network tab but trigger a browser error:
Access to fetch at 'https://api.example.com/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
To resolve this, you must understand the interaction between the browser's security policies and server response headers.
Same-Origin Policy (SOP) is the browser security mechanism that restricts how JavaScript on one origin interacts with resources from another. An origin is the combination of:
| Component | Example |
|---|---|
| Protocol | https:// |
| Domain | api.example.com |
| Port | :443 |
All three must match. If any differ, the request is cross-origin and subject to SOP.
| From | To | Same origin? |
|---|---|---|
http://localhost:3000 | http://localhost:8080 | ❌ Different port |
https://app.com | https://api.app.com | ❌ Different subdomain |
https://app.com | http://app.com | ❌ Different protocol |
SOP prevents malicious sites from exploiting the fact that browsers automatically attach credentials (cookies) to requests. If you are logged into bank.com, the browser sends your session cookies with every request to that domain. Without SOP, a malicious site could trigger a background request to bank.com/transfer and the browser would include your credentials, authorizing the action without your knowledge.
Browsers handle cross-origin requests based on their complexity.
A request is "simple" if:
GET, POST, or HEAD.Accept, Accept-Language, Content-Language, or Content-Type (with values text/plain, multipart/form-data, or application/x-www-form-urlencoded).Simple requests are sent directly to the server without a handshake.
Any request that falls outside the simple criteria is "non-simple." Examples include:
PUT, DELETE, PATCH.Content-Type: application/json.Authorization, X-Api-Key.Non-simple requests trigger a preflight OPTIONS request. The browser verifies server permission before sending the actual request.
The browser does not block a request from leaving the machine or reaching the server. It blocks JavaScript access to the response.
Access-Control-Allow-Origin in the response.
OPTIONS preflight.Cross-Origin Resource Sharing (CORS) is the mechanism servers use to opt-in to cross-origin access.
Access-Control-Allow-Origin: https://app.com
Use * only for public APIs without credentials.
OPTIONS responses for non-simple requests must include:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
If the request includes cookies or Authorization, the server must specify the exact origin and set:
Access-Control-Allow-Credentials: true
The wildcard * is not permitted when credentials are included.
SOP is a browser-only restriction. Tools like curl, Postman, or PostPilot bypass these checks because they are not browsers. If a request works in PostPilot but fails in a web app, the issue is a CORS misconfiguration on the backend.
Determine if your request is simple or non-simple. If it includes Authorization or application/json, you must debug the preflight first.
Manually trigger the preflight using curl:
curl -X OPTIONS https://api.example.com/users \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Authorization" \
-v
Ensure the response contains the required Access-Control-Allow-* headers. In PostPilot, paste the curl command directly into the URL field; it automatically parses the command into a request. Send it and inspect the Headers tab to verify the response.
Ensure the final response (not just the preflight) also contains Access-Control-Allow-Origin.
If you control both the frontend and backend, the standard solution is to serve them from the same origin. By using a reverse proxy (e.g., Nginx) or a framework proxy (e.g., Nuxt/Vite dev proxy), you can map the backend to a relative path.
https://app.comhttps://app.com/apiSince they share the same origin, SOP does not apply and no CORS configuration is required.
If your frontend and backend must reside on different origins (e.g., app.com and api.com), you must configure the backend to explicitly allow cross-origin access.
quarkus.http.cors=true
quarkus.http.cors.origins=http://localhost:3000
quarkus.http.cors.methods=GET,POST,PUT,DELETE
quarkus.http.cors.headers=Authorization,Content-Type
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}
}
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:3000',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
}));
OPTIONS request must succeed before the target request is sent.