How to Debug CORS and Browser API Requests
CORS is a browser security mechanism that controls whether frontend JavaScript may read a cross-origin response. It is not an authentication system, and it usually is not enforced by cURL or server-to-server clients. A request working in cURL but failing in a browser is therefore an important clue, not a contradiction.
Browser messages can be misleading: CORS errors can obscure server responses. The server may have returned a useful 401, 500, or redirect, but without suitable CORS headers the browser prevents JavaScript from reading it.
Identify the actual origins
An origin is the combination of scheme, hostname, and port. These are different origins:
https://app.example.test
https://api.example.test
http://app.example.test
https://app.example.test:8443
In DevTools Network, inspect the request URL, Origin header, status, redirects, and response headers. Do not rely only on the Console summary. Reproduce a sanitized request in the API Workbench to separate HTTP behavior from application code.
Understand preflight
Some cross-origin requests trigger an OPTIONS preflight. The browser asks whether the intended method and headers are allowed:
OPTIONS /v1/widgets HTTP/1.1
Origin: https://app.example.test
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization, content-type
A matching response could be:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.test
Access-Control-Allow-Methods: GET, PUT, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Vary: Origin
The route, reverse proxy, authentication middleware, and CDN must allow OPTIONS to complete. Requiring a bearer token on the preflight commonly causes failure because the browser is asking permission before sending the actual request.
Configure credentials intentionally
For cookie-based cross-origin requests, both client and server must opt in:
const response = await fetch('https://api.example.test/v1/profile', {
credentials: 'include',
});
Access-Control-Allow-Origin: https://app.example.test
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: * cannot be used for a credentialed browser response. Validate an origin against a fixed allowlist; do not blindly copy the request's Origin. Cookies also need appropriate Secure and SameSite attributes. CORS permission does not prevent CSRF, so apply a suitable CSRF design for state-changing cookie-authenticated requests.
Debug in a reliable order
- Confirm the final URL, method, scheme, hostname, and port.
- Check for DNS, TLS, mixed-content, or certificate errors before blaming CORS.
- Inspect whether an
OPTIONSrequest occurs and why it failed. - Verify allowed origin, method, request headers, and credentials exactly.
- Follow redirects: a preflight redirect or login redirect can expose a different CORS policy.
- Inspect gateway, application, and CDN logs using a request ID.
- Test the equivalent HTTP call outside the browser.
The HTTP Headers analyzer helps review the final header set. Convert a known request with cURL to Code when checking client differences, but remember that a generated browser request remains subject to CORS.
Express example
Use a maintained middleware package or implement a narrow allowlist:
import cors from 'cors';
const allowedOrigins = new Set([
'https://app.example.test',
'https://admin.example.test',
]);
app.use(cors({
origin(origin, callback) {
if (!origin || allowedOrigins.has(origin)) return callback(null, true);
callback(new Error('Origin not allowed'));
},
methods: ['GET', 'POST', 'PUT'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true,
maxAge: 600,
}));
Requests without Origin, such as server clients, are handled separately here. Decide whether that is appropriate for your API; CORS does not block them.
Common mistakes
Do not add permissive headers in frontend code—they must come from the responding server or proxy. Browser extensions and mode: 'no-cors' do not fix an API: an opaque response is unreadable and disabling browser protections hides deployment defects. Avoid allowing every origin together with sensitive unauthenticated endpoints, and include Vary: Origin when dynamically selecting a response origin so caches do not serve one tenant's policy to another.
Finally, return CORS headers consistently on error responses where appropriate. Otherwise the browser may report “blocked by CORS” while the actionable server error remains visible only in backend logs.