CORS errors can look like frontend bugs, API failures, or authentication problems. This guide gives you a reusable checklist for diagnosing cross-origin requests, understanding preflight behavior, choosing the right Access-Control-Allow-Origin value, and separating browser policy issues from ordinary server errors.
Overview
Cross-Origin Resource Sharing, or CORS, is the browser-controlled mechanism that determines whether JavaScript running on one origin may read a response from another origin. An origin is the combination of scheme, host, and port. For example, https://app.example.com and https://api.example.com have different origins even though they share a parent domain. A different port or scheme also creates a different origin.
The browser may send a request to the server, but it will prevent frontend JavaScript from reading the response unless the API returns headers that authorize the requesting origin. This is why a request can appear in server logs while the browser console reports a “blocked by CORS policy” error.
A simplified request flow looks like this:
Frontend page
|
| fetch() from https://app.example.com
v
Browser checks cross-origin rules
|
+-- Simple request: send request, inspect response headers
|
+-- Non-simple request: send OPTIONS preflight first
|
+-- Approved: send actual request
+-- Rejected: block the frontend from proceeding
CORS is enforced by browsers. A command-line client, server-side job, or many API testing tools may not enforce the same browser read restrictions. Therefore, a request succeeding in an API client does not prove that a browser application is configured correctly.
Checklist by scenario
1. A basic GET request is blocked
- Record the exact frontend origin, including scheme and port.
- Confirm that the API response includes
Access-Control-Allow-Origin. - Use the exact allowed origin, such as
https://app.example.com, rather than assuming a hostname alone is sufficient. - Check whether a redirect sends the request to a different host or path.
- Inspect the response in the browser’s Network panel, not only the console message.
A typical response for one known frontend might include:
Access-Control-Allow-Origin: https://app.example.com
If multiple frontend origins are supported, the server should compare the incoming Origin header against an explicit allowlist and return the matching origin. It should not blindly reflect every origin.
2. The browser sends an OPTIONS preflight request
A CORS preflight request is commonly triggered when the frontend uses methods such as PUT, PATCH, or DELETE, sends non-safelisted headers, or uses a content type that requires negotiation. The browser sends an OPTIONS request describing the intended method and headers.
The server must answer the preflight with compatible permissions, for example:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Use this decision path:
- Does the server route accept
OPTIONSfor the requested URL? - Does the response include the requesting origin?
- Is the requested method listed in
Access-Control-Allow-Methods? - Are all requested non-simple headers listed in
Access-Control-Allow-Headers? - Do authentication middleware, redirects, or a reverse proxy remove the CORS headers?
3. Cookies or other credentials are involved
For cookie-based sessions, the frontend request generally needs credentials enabled, such as fetch(url, { credentials: 'include' }). The server must then return:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Credentialed CORS cannot use a wildcard origin. Replace * with a specific permitted origin, and check the cookie’s own attributes and scope separately. CORS headers do not make an incorrectly scoped, expired, or blocked cookie available.
When using bearer tokens in an Authorization header, verify both the preflight permission for that header and the API’s authentication logic. A token can be correctly formed while the request still fails at the browser’s CORS stage. For related token troubleshooting, see the JWT decoder and validator guide.
4. Development works through a proxy but fails in production
A development server may proxy /api requests so the browser sees a same-origin URL. That can hide the API’s real cross-origin configuration. In production, document the actual frontend and API origins, then test the deployed route directly.
- Check environment variables for the frontend API base URL.
- Confirm that staging and production use the intended allowlists.
- Inspect the edge proxy, load balancer, or web server for header changes.
- Ensure error responses also receive the relevant CORS headers.
What to double-check
Start with the browser’s Network panel and compare the request with the server’s CORS policy. The most useful details are the request’s Origin, method, requested headers, status code, redirect chain, and response headers.
For a framework configuration, keep the policy close to the API boundary and make the allowed origins explicit. A conceptual Express-style setup might look like this:
const allowedOrigins = new Set([
'https://app.example.com',
'https://staging-app.example.com'
]);
// CORS middleware should allow a matching origin
// and handle OPTIONS before protected route logic.
The exact configuration depends on the framework and middleware, but the decisions remain the same: identify trusted origins, permit only required methods and headers, decide whether credentials are needed, and ensure preflight requests reach a valid response.
Also check caching. If a response varies by the incoming Origin, an intermediary may need a Vary: Origin response directive so that one origin’s cached response is not incorrectly reused for another. Treat this as part of the deployment configuration, especially when a CDN or reverse proxy sits in front of the API.
Do not confuse CORS with server authorization. CORS controls whether browser JavaScript may read a response; authentication and authorization determine whether the server should perform or return the requested operation. Both layers need to be correct.
Common mistakes
- Adding CORS headers only to successful responses: authentication failures, validation errors, and server errors can also be inspected by frontend code and should be handled consistently.
- Using
*with credentials: wildcard origins and credentialed browser requests are not a compatible combination. - Allowing the wrong origin:
http://localhost:3000,http://localhost:5173, andhttps://localhost:3000are different origins. - Ignoring preflight: configuring the actual
POSTroute does not automatically configure itsOPTIONSrequest. - Allowing headers that are not actually needed: keep methods and headers narrow and review them when the frontend changes.
- Testing only with curl: a successful command-line response does not reproduce every browser CORS check.
- Trying to fix CORS in frontend JavaScript: request mode settings cannot grant permission that the server has not returned.
- Using a public proxy as a permanent fix: this can expose data, complicate authentication, and obscure the real API configuration.
If request payloads are difficult to inspect, format the response or configuration with a JSON formatter online. Clear JSON makes it easier to distinguish an API error body from a browser-generated CORS failure.
When to revisit
Recheck this CORS checklist whenever the frontend host, API host, scheme, port, authentication method, or deployment path changes. It is especially useful before promoting a staging build, moving an API behind a new proxy, introducing a CDN, adding a new custom request header, or changing a cookie-based session to token-based authentication.
For a practical retest, open a fresh browser session and verify one request from each supported frontend origin. Test a simple request, a request that triggers preflight, an authentication failure, and a successful credentialed request if your application uses cookies. Record the expected origin, methods, headers, and credential behavior in deployment documentation.
Finally, remove temporary development allowances before release. Keep the production allowlist deliberate, test it as part of frontend and API integration checks, and review it whenever workflows or tools change. That turns CORS from a recurring console surprise into a small, visible part of the application’s interface contract.