CORS errors are usually configuration mismatches, not mysterious browser failures. This guide gives you a reusable workflow for tracing browser-to-API requests, fixing preflight responses, handling credentials safely, and checking deployment settings across common web stacks.
Overview
Cross-Origin Resource Sharing (CORS) is the browser’s way of enforcing a server-approved boundary between origins. 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 local frontend at http://localhost:5173 is also a different origin from an API at http://localhost:8000.
A browser may send the request to the server, but refuse to expose the response to JavaScript unless the response contains an appropriate Access-Control-Allow-Origin header. This is why a request can appear successful in server logs while the frontend reports a CORS error.
Browser-to-server request flow
Frontend JavaScript
|
| 1. Simple request, or OPTIONS preflight
v
Browser --------------------> API server
| |
| | Checks Origin, method,
| | headers, and credentials
| |
| 2. Response with CORS headers
<-------------------------
|
| 3. Browser exposes or blocks
| the response for JavaScriptA request is more likely to trigger a preflight when it uses a method such as PUT, PATCH, or DELETE, sends non-simple request headers, or uses a content type outside the browser’s simple-request rules. The preflight is an OPTIONS request containing headers such as Origin, Access-Control-Request-Method, and Access-Control-Request-Headers.
CORS is not an authentication system. It does not protect an API from command-line clients, server-side programs, or malicious requests that omit browser enforcement. Use authentication, authorization, CSRF protections where applicable, rate limits, and input validation separately.
Checklist by scenario
Scenario 1: A basic GET request is blocked
- Open the browser’s Network panel and inspect the failed request.
- Record the exact request
Origin, including scheme, hostname, and port. - Check whether the API response includes
Access-Control-Allow-Origin. - Confirm that its value exactly matches the allowed frontend origin, or is an intentionally broad policy for a non-credentialed public endpoint.
- Check redirects. A redirect from the API to a login page, another hostname, or an HTTP endpoint can create a separate CORS failure.
Scenario 2: An OPTIONS preflight fails
- Find the
OPTIONSrequest immediately before the failed request. - Confirm the server, reverse proxy, or platform routes
OPTIONSto a response rather than rejecting it at authentication or method filtering. - Return the requested method in
Access-Control-Allow-Methods. - Return every requested non-simple header in
Access-Control-Allow-Headers, such asAuthorizationorContent-Type. - Return a successful status and a complete CORS response. A server may use
204 No Contentor another successful response appropriate to its framework.
Scenario 3: Cookies or other credentials are involved
- Set
credentials: 'include'in the browser request when cross-origin cookies are intended. - Return a specific origin in
Access-Control-Allow-Origin; do not combine*withAccess-Control-Allow-Credentials: true. - Return
Access-Control-Allow-Credentials: trueon the relevant response and, where needed, the preflight response. - Check cookie attributes separately. Cross-site cookie behavior can depend on
SameSite,Secure, domain, path, and browser policy. - Verify that the API’s session or authentication middleware does not reject
OPTIONSbefore CORS handling runs.
Scenario 4: It works locally but fails after deployment
- Compare the deployed frontend origin with the local origin. A production hostname, preview URL, and custom domain are separate origins.
- Check the production reverse proxy, CDN, serverless function, and API gateway independently.
- Inspect redirects caused by HTTP-to-HTTPS rules, trailing-slash rules, or authentication middleware.
- Confirm that environment variables contain the deployed API URL and an allowed origin, rather than a local development value.
- Test from the actual browser origin instead of relying only on an API client that does not enforce CORS.
Quick decision tree
Is the error reported by browser JavaScript? If no, investigate the network, server, or application error first. If yes, continue. Is there an OPTIONS request? If yes, fix its status and CORS response headers before investigating the actual request. If no, inspect the actual response for Access-Control-Allow-Origin. Are cookies or credentials used? If yes, use an explicit origin and credential-compatible cookie settings. If no, check the origin, method, and requested headers. Does the response pass through a proxy or CDN? If yes, verify that it preserves CORS headers and varies cached responses by Origin when the allowed origin is dynamic.
Header reference
| Header | Purpose | What to check |
|---|---|---|
Origin | Identifies the requesting frontend origin. | Use its exact scheme, host, and port when comparing allowlists. |
Access-Control-Allow-Origin | States which origin may read the response. | Use one explicit origin per response; do not send a list of origins. |
Access-Control-Allow-Methods | Allows methods requested during preflight. | Include the method the browser requested. |
Access-Control-Allow-Headers | Allows request headers named by the preflight. | Include headers such as Authorization and Content-Type when needed. |
Access-Control-Allow-Credentials | Allows the browser to include and expose credentialed cross-origin requests. | Use true only with an explicit allowed origin. |
Access-Control-Max-Age | Lets the browser cache a successful preflight for a period. | Keep the value moderate while debugging; cached preflights can hide configuration changes. |
Vary: Origin | Helps caches distinguish responses generated for different origins. | Use it when the server dynamically reflects an approved origin. |
Copy-paste configurations
These are starting patterns, not universal drop-in policies. Replace the example origin with the frontend origin you actually intend to allow, and keep the allowlist as narrow as the application permits.
Express
import cors from 'cors';
app.use(cors({
origin: ['https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 600
}));Register CORS before routes that need it. If the origin is selected dynamically, validate it against an allowlist and ensure the response varies by Origin.
Nginx
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin 'https://app.example.com' always;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
add_header Access-Control-Allow-Headers 'Content-Type, Authorization' always;
add_header Access-Control-Allow-Credentials 'true' always;
add_header Access-Control-Max-Age 600 always;
return 204;
}
add_header Access-Control-Allow-Origin 'https://app.example.com' always;
add_header Access-Control-Allow-Credentials 'true' always;
proxy_pass http://api_upstream;
}Use care with Nginx conditionals and inherited add_header directives. Confirm both preflight and ordinary responses in the Network panel.
Apache
<IfModule mod_headers.c>
Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
Header always set Access-Control-Allow-Credentials "true"
</IfModule>Make sure Apache or the upstream application responds to OPTIONS, and ensure the required headers are not removed by another virtual host or proxy rule.
Django
# settings.py
INSTALLED_APPS = [
# ...
'corsheaders',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
# ...
]
CORS_ALLOWED_ORIGINS = [
'https://app.example.com',
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
'accept', 'content-type', 'authorization',
]Place the CORS middleware early enough to add headers to responses produced by other middleware. Keep CSRF configuration separate; allowing a CORS origin does not automatically authorize state-changing browser requests.
Laravel
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['https://app.example.com'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['Content-Type', 'Authorization'],
'exposed_headers' => [],
'max_age' => 600,
'supports_credentials' => true,
];After changing configuration, clear the framework’s cached configuration when appropriate for your deployment. If using cookie-based authentication, configure the session domain, secure transport, and CSRF flow as a separate checklist.
Serverless functions
export default async function handler(request) {
const origin = 'https://app.example.com';
const headers = {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET,POST,PUT,PATCH,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '600'
};
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers });
}
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { ...headers, 'Content-Type': 'application/json' }
});
}For a deployed function, apply the same pattern in the platform’s runtime or middleware. If a CDN, gateway, or edge proxy sits in front of the function, check that it forwards OPTIONS and preserves the response headers.
What to double-check
- Origin spelling:
https://app.example.com,http://app.example.com, andhttps://app.example.com:8443are different origins. - Response coverage: Add CORS headers to successful responses and relevant error responses, not only the happy path.
- Credentials: A wildcard origin is incompatible with credentialed browser requests. Use an explicit allowlist.
- Headers versus exposed headers:
Access-Control-Allow-Headerscontrols what the browser may send.Access-Control-Expose-Headerscontrols which response headers frontend JavaScript may read. - Cache behavior: If the server reflects approved origins, include
Vary: Originand inspect CDN caching rules. - Preflight caching: A browser may reuse a successful preflight. Clear site data, wait for the cache to expire, or temporarily reduce
Access-Control-Max-Agewhile testing. - Authentication redirects: An unauthenticated
OPTIONSrequest should not be redirected to a login page. - Tool limitations: curl, API clients, and server-to-server tests can confirm server responses but do not reproduce all browser CORS enforcement.
Common mistakes
The most common mistake is adding Access-Control-Allow-Origin: * everywhere without checking whether the application uses cookies or authorization-sensitive browser requests. Another is allowing the origin but forgetting that a preflight also needs the requested method and headers. Adding CORS only in the frontend cannot fix the problem: the API or the layer serving the API must provide the response headers.
It is also easy to test the wrong URL. A frontend may call a development API while the deployed build uses an environment variable, a proxy path, or a different subdomain. Inspect the final request URL rather than assuming the source configuration is active.
Finally, avoid treating CORS as a substitute for access control. A permissive CORS policy may be appropriate for a genuinely public read-only endpoint, but it does not make private data safe. Authentication and authorization must still be enforced on the server.
When to revisit
Keep this checklist with the deployment workflow and revisit it whenever a frontend changes domain, a new preview environment is added, an API moves behind a proxy or CDN, authentication changes, or a new method or request header is introduced. It is especially useful to repeat the check after switching frameworks, serverless runtimes, hosting providers, or deployment pipelines, because the layer handling OPTIONS may have changed.
For a practical final pass, test one simple request, one preflighted request, and one credentialed request if your application uses cookies. Test them from the production origin and from each supported preview origin. Save the browser Network-panel details, the server response headers, and the configured allowlist with the project’s deployment notes. That record makes the next browser API request blocked error faster to diagnose and reduces the chance of fixing only the local environment.