CORS Errors Explained: Debug and Fix Them
You ship a new frontend, open the console, and there it is: a red CORS error saying the request was “blocked by CORS policy.” Your API works in Apidog or curl, yet the browser refuses to give your JavaScript the response. The problem is frustrating, but the fix becomes straightforward once you know where the error lives.
The key fact: CORS is enforced by the browser but caused by the server. The browser blocks the response because the server did not send the required Access-Control-Allow-Origin headers. The fix usually belongs in server configuration—not frontend code.
This guide explains:
- What CORS does
- How preflight requests work
- The six most common CORS errors and their fixes
- Working configurations for Express, Spring Boot, and Nginx
- How to debug CORS outside the browser
What a CORS error is—and isn’t
CORS stands for Cross-Origin Resource Sharing.
Browsers enforce the same-origin policy by default. JavaScript running on https://app.example.com cannot read responses from https://api.example.com because the scheme, host, or port differs. CORS lets a server relax that restriction intentionally.
See the MDN CORS documentation and the Fetch specification for the complete definitions.
Three facts explain most CORS confusion:
- The browser enforces CORS. Server-to-server calls, curl, and desktop API clients do not apply browser CORS checks.
- The server configures CORS. The browser makes its decision from the response headers sent by the server.
- The request may still reach the server. For simple requests, the server can process and respond normally. The browser then withholds the response from JavaScript.
CORS is not an authentication layer or an API firewall. It protects users from malicious web pages reading cross-origin data with their cookies.
When you see a CORS error, inspect the message and fix the missing or incorrect server header instead of adding a frontend workaround.
How the preflight request works
Before certain cross-origin requests, the browser sends an OPTIONS request called a preflight.
A preflight is triggered when the request:
- Uses methods other than
GET,HEAD, orPOST - Sends custom headers such as
Authorization - Uses a content type such as
application/json
Example preflight:
OPTIONS /v1/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
The browser is asking whether a page on app.example.com may send a POST request with those headers.
A valid response looks like this:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400
Vary: Origin
If the preflight fails, the browser cancels the actual request before it runs. Your endpoint may never execute, and your logs may show only the OPTIONS request.
Access-Control-Max-Age tells the browser how long to cache the preflight result. In this example, it is cached for 86,400 seconds.
For every CORS issue, first determine whether the preflight failed or the actual request failed.
Six common CORS errors and their fixes
1. No Access-Control-Allow-Origin header is present
The server returned no CORS headers, so the browser had nothing to evaluate.
Configure the server to return either the requesting origin or * for public, credential-free APIs:
Access-Control-Allow-Origin: https://app.example.com
A common trap is missing CORS headers on error responses. If successful responses include CORS headers but 500, 403, or 401 responses do not, the browser may display a CORS error instead of the real API error.
Apply CORS headers to every response.
2. Wildcard * cannot be used with credentials
The browser reports:
The value of the ‘Access-Control-Allow-Origin’ header must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’.
This happens when the frontend sends cookies or authentication data with credentials: 'include', while the server responds with:
Access-Control-Allow-Origin: *
Credentials and a wildcard origin cannot be combined.
Use the exact origin and enable credentials:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Always validate the incoming Origin against an allowlist before echoing it. Reflecting arbitrary origins while allowing credentials defeats the protection.
3. Response to preflight request doesn’t pass access control check
The server did not handle OPTIONS correctly. Common causes include:
- The route defines
POSTbut notOPTIONS -
OPTIONSreturns404or405 - Authentication middleware rejects the preflight with
401
Browsers do not attach credentials to preflight requests.
Handle OPTIONS before authentication:
app.options('/v1/orders', (req, res) => {
res.set({
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Authorization, Content-Type'
});
res.sendStatus(204);
});
In most frameworks, mounting CORS middleware before authentication handles this automatically.
4. The header value is not equal to the supplied origin
The server sends Access-Control-Allow-Origin, but its value does not exactly match the requesting origin.
Typical causes:
- Production is hardcoded while testing from
http://localhost:5173 -
httpandhttpsare treated as equivalent - The origin contains a trailing slash
https://app.example.com/ is not a valid match for the origin https://app.example.com.
Compare origins exactly and include Vary: Origin so caches and CDNs do not reuse one origin’s response for another:
const allowed = ['https://app.example.com', 'http://localhost:5173'];
if (allowed.includes(req.headers.origin)) {
res.set('Access-Control-Allow-Origin', req.headers.origin);
res.set('Vary', 'Origin');
}
5. Request header field or method is not allowed
You may see either of these messages:
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight responseMethod PUT is not allowed by Access-Control-Allow-Methods
The preflight reached the server, but the response did not allow every method or header used by the frontend.
For example, if the frontend sends Authorization or X-Request-Id, include them in the response:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-Id
Header names are case-insensitive. HTTP methods are case-sensitive and conventionally uppercase.
6. Redirect is not allowed for a preflight request
The preflight received a 301 or 302 response. Browsers generally refuse to follow redirects during preflight.
Common causes include:
- An HTTP URL redirecting to HTTPS
- A missing trailing slash
- A gateway redirecting
/v1/ordersto/v1/orders/
Point the frontend to the final URL directly. Use HTTPS from the beginning, follow your router’s trailing-slash convention, and verify that OPTIONS returns 2xx rather than 3xx.
Server configuration examples
Express
Use the official cors middleware instead of manually managing headers:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({
origin: ['https://app.example.com', 'http://localhost:5173'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 86400
}));
Mount it before authentication middleware so preflights are not rejected for missing tokens.
Python developers can use the Flask-CORS extension, which provides the same header-handling pattern for Flask applications.
Spring Boot
Configure CORS globally with WebMvcConfigurer:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/v1/**")
.allowedOrigins("https://app.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.allowCredentials(true)
.maxAge(86400);
}
}
If you use Spring Security, also call .cors(Customizer.withDefaults()) in the security filter chain. Otherwise, the security layer may block preflights before MVC configuration handles them.
See the Spring CORS documentation for all available options.
Nginx
When Nginx terminates requests in front of your application, handle preflights at the edge:
location /v1/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
return 204;
}
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Vary "Origin" always;
proxy_pass http://backend;
}
The always flag is important. Without it, Nginx omits add_header directives on 4xx and 5xx responses, causing the missing-header problem on failed requests.
Choose one layer to own CORS. If both Nginx and the application add headers, the browser may receive duplicates such as:
Access-Control-Allow-Origin: *, *
and reject the response.
Debug CORS outside the browser with Apidog
The browser console tells you that access was blocked, but not exactly what the server returned. The fastest way to inspect the real response is to remove the browser from the test.
Apidog is a desktop API client, so it is not subject to browser CORS checks. Replaying the same request there gives you a clean comparison:
- If it succeeds in Apidog, your API logic likely works and the problem is missing or incorrect CORS headers.
- If it fails in Apidog too, you have an ordinary API problem rather than a CORS problem.
Use this workflow:
-
Replay the actual request. Copy the failing request from the browser’s Network tab into Apidog with the same method, headers, and body. Check the status and response body. A
500means CORS was not the underlying problem. -
Test the preflight manually. Create an
OPTIONSrequest with these headers:
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
-
Inspect response headers. Check
Access-Control-Allow-Origin,Access-Control-Allow-Methods, andAccess-Control-Allow-Headers. Look for missing values, incorrect origins, and3xxresponses. -
Verify the fix. Resend the saved
OPTIONSrequest after changing the server configuration and confirm that the headers are correct.
This also explains why an API client can work while the browser fails: desktop clients skip CORS, while browsers require the server to explicitly opt in.
You can download Apidog for free and save the OPTIONS request alongside your regular endpoint tests.
A 30-second CORS checklist
Before filing a bug, verify:
- Does the failing response include
Access-Control-Allow-Origin? - Does its value exactly match the page origin, including scheme, host, and port, with no trailing slash?
- If using cookies or authentication, is there a specific origin plus
Access-Control-Allow-Credentials: true—never*? - Does
OPTIONSreturn2xxwith methods and headers covering the request? - Does the preflight URL redirect?
- Do
401,403, and500responses include the same CORS headers as successful responses?
Most CORS issues match one of these checks. Verify the behavior with a manual OPTIONS request in Apidog, update the server configuration, and continue building.
FAQ
Why do I get a CORS error only in the browser?
Only browsers enforce CORS. The same-origin policy prevents malicious pages from reading users’ authenticated data, so browsers check Access-Control-Allow-Origin on cross-origin responses.
curl, backend services, and desktop clients do not apply this restriction. If the request succeeds everywhere except the browser, the server is probably missing or misconfiguring CORS headers.
Does CORS apply to Postman or Apidog?
No. Postman and Apidog are desktop applications, not web pages running inside a browser sandbox, so their requests bypass CORS.
That makes them useful for debugging: they expose the server’s raw response headers without browser filtering. A passing request in a desktop client does not prove browser compatibility, but it helps isolate the failing layer.
Is a CORS error a security feature or a bug?
It is a browser security feature. A CORS error means the browser is refusing to expose cross-origin response data because the server has not opted in.
Disabling CORS with browser flags or extensions only hides the problem on your machine. Fix the server headers instead.
Can I use Access-Control-Allow-Origin: * everywhere?
Only for public, read-only APIs that do not use cookies or authentication.
The wildcard is rejected when credentials are included, and it allows every web origin to read the response. For authenticated APIs, maintain an origin allowlist, echo the matching origin, and send Vary: Origin so shared caches keep responses separated.
Top comments (0)