CORS Error Fix: What Each Header Does and How to Configure It Correctly

Quick answer

💡A CORS error means the browser blocked a cross-origin request because the server did not return the required Access-Control-Allow-Origin header. CORS is enforced entirely by the browser — curl and server-to-server calls are never affected. The fix must be made on the server side by adding the correct headers to the response. If you use credentials, you cannot use a wildcard origin and must set Access-Control-Allow-Credentials: true alongside the exact origin.

Error symptoms

  • Browser console shows: Access to fetch blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
  • Network tab shows the OPTIONS preflight returning 404, 405, or 401 before the real request fires
  • Request works fine in curl or Postman but fails in every browser
  • Adding credentials: 'include' to fetch causes the request to fail even when it worked without credentials
  • Server response includes Access-Control-Allow-Origin: * but the request still fails
  • Error message says the request header field Authorization is not allowed by Access-Control-Allow-Headers

Common causes

  • Server does not return Access-Control-Allow-Origin on the response at all
  • CORS middleware is registered after authentication middleware, so OPTIONS preflights are rejected with 401
  • Access-Control-Allow-Origin is set to wildcard (*) while the client also uses credentials: 'include' — these are incompatible
  • Access-Control-Allow-Headers does not list a custom header like Authorization or X-Request-ID that the client sends
  • nginx or a CDN strips CORS headers from the upstream response before the browser sees them
  • The server returns CORS headers on the OPTIONS preflight but forgets to add them to the actual GET or POST response

When it happens

  • First time calling a backend API from a frontend on a different domain or port
  • After adding an Authorization header to an existing API call that previously used no custom headers
  • After deploying a frontend to a CDN subdomain while the API stays on the root domain
  • When migrating from a same-origin proxy setup like Next.js API routes to direct cross-origin API calls
  • After enabling HTTPS on the frontend while the API still runs on HTTP, since protocol is part of the origin

Examples and fixes

A bare Express server that handles API routes but sends no CORS headers, blocking every browser request from a different origin.

Express app missing CORS headers entirely

❌ Wrong

const express = require('express');
const app = express();

app.use(express.json());

app.post('/api/users', async (req, res) => {
  const user = await db.create(req.body);
  res.json(user);
});

app.listen(3001);

✅ Fixed

const express = require('express');
const cors = require('cors');
const app = express();

const corsOptions = {
  origin: process.env.ALLOWED_ORIGIN,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
  credentials: true,
  maxAge: 600
};

app.use(cors(corsOptions));
app.options('*', cors(corsOptions));
app.use(express.json());

app.post('/api/users', async (req, res) => {
  const user = await db.create(req.body);
  res.json(user);
});

app.listen(3001);

The broken version has no CORS configuration at all. Every request from a browser on a different origin is blocked before the response body is ever read. The fix installs the cors npm package before express.json() and before any authentication middleware. The explicit app.options('*', cors(corsOptions)) call handles preflight OPTIONS requests at all routes. Setting maxAge: 600 caches the preflight result in the browser for 10 minutes, reducing round trips. Reading the allowed origin from an environment variable means you can configure different values for development, staging, and production without code changes. Setting credentials: true enables cookie and Authorization header support, but requires origin to be an exact value — never a wildcard string when credentials are in use.

A server that uses Access-Control-Allow-Origin: * works for anonymous calls but breaks the moment the client adds cookies or an Authorization header.

Wildcard origin blocking credentialed requests

❌ Wrong

// Server response headers
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/json

// Client fetch call
fetch('https://api.example.com/profile', {
  credentials: 'include',
  headers: {
    'Authorization': 'Bearer eyJhbGci...'
  }
});

✅ Fixed

// Server — read and reflect the request origin
app.use((req, res, next) => {
  const origin = req.headers.origin;
  const allowedOrigins = (process.env.ALLOWED_ORIGINS || '').split(',');
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Vary', 'Origin');
  }
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

The CORS specification explicitly forbids the combination of Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true. When a fetch call uses credentials: 'include' or sends an Authorization header, the browser requires the server to reflect the exact origin of the request, not a wildcard. The fix reads the Origin header from the incoming request, checks it against an allowlist stored in an environment variable, and echoes it back as the Access-Control-Allow-Origin value. The Vary: Origin header instructs CDNs and caches to store separate cached responses for each origin, preventing one origin's response from being served to a different origin. Setting Access-Control-Allow-Credentials: true completes the server side of the credentials handshake.

An nginx proxy that does not pass CORS headers from the upstream app to the browser, causing CORS failures even when the app is configured correctly.

nginx stripping CORS headers from the upstream response

❌ Wrong

# nginx.conf — passes proxy but loses CORS headers on error responses
server {
  location /api/ {
    proxy_pass http://localhost:3001;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

✅ Fixed

# nginx.conf — handle OPTIONS and preserve CORS headers
server {
  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, 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;
      add_header Content-Length 0;
      return 204;
    }

    add_header Access-Control-Allow-Origin 'https://app.example.com' always;
    add_header Access-Control-Allow-Credentials 'true' always;
    proxy_pass http://localhost:3001;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

Without the always flag on add_header directives, nginx only adds headers to 2xx and 3xx responses. Error responses from the upstream — 4xx and 5xx status codes — do not receive the CORS headers, so the browser cannot read the error body and the network request appears to fail silently. The always flag forces nginx to add the CORS headers to every response regardless of status code. The OPTIONS block handles preflight at the nginx layer, which is more efficient than forwarding OPTIONS to the upstream application. The Access-Control-Allow-Origin header must also be added to non-OPTIONS responses so the browser accepts the actual API response, not just the preflight.

How the browser's same-origin policy works

The same-origin policy is a browser security rule that prevents JavaScript running on one origin from reading responses from a different origin. An origin is defined as the combination of protocol, hostname, and port. https://app.example.com and https://api.example.com are different origins because the hostname differs, even though they share the same parent domain. http://localhost:3000 and http://localhost:3001 are different origins because the port differs. The same-origin policy exists to prevent a malicious website from using your browser session to make authenticated requests to your bank or email provider and reading the response.

Cross-Origin Resource Sharing is the mechanism that lets servers opt in to accepting requests from specific other origins. A server opts in by returning the Access-Control-Allow-Origin header in its response. Without this header, the browser completes the underlying TCP request and receives the full response, but refuses to expose the response body or headers to the JavaScript code that initiated the fetch. This is why the CORS error appears in the browser but the server's access logs show the request as successful — the data was sent and received at the network level, but the browser suppressed it.

CORS distinguishes between two categories of requests. Simple requests use GET, HEAD, or POST with a limited set of allowed headers and no custom headers like Authorization. For simple requests, the browser sends the request directly and checks the Access-Control-Allow-Origin header in the response. Non-simple requests — which include any request with an Authorization header, a Content-Type of application/json, or any custom header — trigger a preflight. The browser first sends an OPTIONS request asking the server to declare which origins, methods, and headers it accepts. If the server's OPTIONS response does not authorize the real request, the browser blocks it without sending it at all.

Because CORS is enforced by the browser and not by the network, tools like curl, Postman, wget, and server-to-server HTTP clients are completely unaffected. A curl command to the same URL will always succeed even when every browser returns a CORS error. This is why the fix must always be applied to the server's response headers — there is no client-side workaround that works in a browser outside of development proxy setups.

Reading the CORS error to find the exact cause

Open the browser DevTools and go to the Console tab. The CORS error message contains specific information about what header or condition failed. Chrome's error messages are the most descriptive: 'No Access-Control-Allow-Origin header is present' means the header is missing entirely. 'The value of the Access-Control-Allow-Origin header does not match the supplied origin' means the header is present but contains the wrong value. 'Request header field Authorization is not allowed by Access-Control-Request-Headers' means the custom header was not listed in the server's Access-Control-Allow-Headers.

Go to the Network tab and filter for OPTIONS to find the preflight request. Click on it and examine the Response Headers section. Confirm that Access-Control-Allow-Origin is present and matches your frontend's origin exactly, including protocol and port. Check that Access-Control-Allow-Headers lists every header your fetch call sends — if your fetch sends an Authorization header but the server only lists Content-Type, the preflight fails. Check the status code: 404 means no OPTIONS route exists; 401 means authentication middleware is running before CORS middleware.

If the OPTIONS preflight shows correct headers but the actual request still fails, click on the actual GET or POST request and check its response headers. Many server configurations correctly handle OPTIONS but forget to add CORS headers to the main response. The browser requires Access-Control-Allow-Origin on every response, not just the preflight.

Use /tools/cors-tester on ToolDock to inspect the CORS headers on your endpoint from an external perspective. This is faster than setting up curl with the correct Origin header manually and shows you exactly what the browser would see. Use /tools/http-request-builder to replay the failing request with the exact headers your frontend sends and observe the raw response headers before any browser filtering occurs.

For local development, check whether your development server is adding CORS headers. Next.js API routes on localhost:3000 calling the same-origin are not cross-origin. But when your React app on localhost:3000 calls an Express server on localhost:3001, that is cross-origin. Confirm the port numbers are different. Also check browser extensions — some ad blockers and privacy tools modify or strip CORS-related headers, causing failures that only affect one browser profile.

Configuring CORS correctly on every server type

At the nginx or reverse-proxy layer, CORS configuration must handle two separate concerns: the preflight OPTIONS response and the CORS headers on all other responses. Add an if block inside your location block that checks $request_method = OPTIONS, returns 204, and includes Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, and Access-Control-Max-Age. Then add Access-Control-Allow-Origin and Access-Control-Allow-Credentials outside the OPTIONS block as well, on every non-OPTIONS response. Critical: use the always flag on every add_header directive. Without it, nginx omits those headers from 4xx and 5xx responses, so the browser cannot read error bodies and treats every server-side failure as a CORS failure. Reload nginx with nginx -s reload after changes to apply without downtime.

For AWS API Gateway, enable CORS on each resource in the Gateway console or in your CloudFormation or Terraform definition. API Gateway can auto-generate the OPTIONS method response, but this only covers the gateway layer. Your Lambda function must also return the CORS headers — Access-Control-Allow-Origin, Access-Control-Allow-Credentials, and any others — on every response it sends. If either layer is missing headers, the browser blocks the response. After deploying, test in the actual browser with DevTools open, because the API Gateway console's built-in test tool does not enforce CORS and will show success even when the browser fails.

For Cloudflare, add CORS response headers via Transform Rules in the dashboard. Create a rule matching your API path pattern that adds the required headers to the response. For dynamic origin reflection — where the allowed origin differs per request — use a Cloudflare Worker that reads the Origin request header, checks it against an allowlist, and sets Access-Control-Allow-Origin to the matching origin before forwarding to your origin server. Always set Vary: Origin on responses that reflect the origin dynamically; without it, Cloudflare's cache can serve a response cached for one origin to a request from a different origin.

In Python applications using FastAPI, add CORSMiddleware before other middleware: app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]). Django REST Framework uses the django-cors-headers package with CORS_ALLOWED_ORIGINS in settings.py. For any Python framework, confirm the middleware is registered before authentication and session middleware in the stack order.

For Node.js applications not using Express, set the headers directly on the response object: res.setHeader('Access-Control-Allow-Origin', allowedOrigin) and handle OPTIONS with an early return that sets status 204. If you use a framework like Fastify, use the @fastify/cors plugin with the same origin-function approach. Verify with /tools/cors-tester after deploying any configuration change.

Credential and cache edge cases in CORS

The wildcard-and-credentials incompatibility is the most common CORS edge case. When a fetch call uses credentials: 'include' or sends any credentialed header, the CORS specification requires the server to respond with the exact requesting origin rather than an asterisk. If your server returns Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true simultaneously, Chrome and Firefox both reject the response and show a specific error message about this incompatibility. Fix it by reading the Origin request header and echoing it back if it is on your allowlist.

Vary: Origin is a critical response header when you use dynamic origin reflection. Without it, a CDN or reverse proxy may cache a response with Origin: https://app1.example.com and serve it to requests from https://app2.example.com. The cached response contains Access-Control-Allow-Origin: https://app1.example.com which does not match the second request's origin. This causes CORS errors that are intermittent and hard to reproduce. Setting Vary: Origin tells every cache layer to store separate versions of the response for each origin value.

The null origin is an unusual case that catches teams when working with local file:// pages or certain redirected requests. When Origin: null appears in the request headers, setting Access-Control-Allow-Origin: null seems like the correct fix, but the CORS specification warns against it. The null origin can be spoofed by sandboxed iframes and should not be placed in an allowlist. If you need to support local development from file:// pages, use a local web server instead of loading the file directly.

Preflight caching via Access-Control-Max-Age reduces the number of OPTIONS requests the browser sends. Chrome caches preflight results for a maximum of 7200 seconds regardless of the value you set; Firefox has a lower maximum. Setting maxAge: 600 in your cors options is a good default that reduces preflight overhead without locking users into stale CORS configurations for too long. After changing CORS headers on a deployed server, users whose browsers have cached the old preflight result may continue to see failures until the cache expires.

Subdomain wildcards in Access-Control-Allow-Origin are not supported by the CORS specification. You cannot set Access-Control-Allow-Origin: *.example.com. If you need to support multiple subdomains, maintain an explicit list of allowed origins and reflect the matching one dynamically. The cors npm package's origin option accepts an array of strings, a function, or a regular expression, all of which can match multiple subdomains.

CORS configuration mistakes that cause silent failures

Placing cors() middleware after authentication middleware is the single most common CORS configuration mistake. When a browser sends an OPTIONS preflight, it intentionally omits credentials — no cookies, no Authorization header. Authentication middleware that requires a valid token will reject the OPTIONS request with 401. The browser receives the 401 on the preflight and considers the preflight failed, blocking the actual API call before it is even sent. The error in the browser looks like a CORS failure, not an authentication failure, which obscures the root cause. The fix is always to register cors() before any middleware that returns early responses.

Adding CORS headers only to some routes creates inconsistent behavior that is difficult to debug. If cors() is registered on /api/data but not on /api/auth, cross-origin requests to /api/auth succeed during the preflight check but then the actual request fails when the response has no CORS headers. Apply cors() globally at the application level and restrict the allowed origins configuration rather than restricting which routes have CORS enabled.

Forgetting to add CORS headers to error responses is another frequent mistake. When an API endpoint returns a 400 or 500 status, nginx without the always flag on add_header will not add CORS headers to the error response. The browser receives the error status but cannot read the response body because it lacks CORS headers. The developer sees a CORS error instead of the actual validation error. Always use the always flag in nginx add_header directives for CORS-related headers.

Testing CORS fixes only with curl or Postman gives a false confidence that the fix is working. These tools bypass CORS enforcement entirely. A curl test that succeeds proves nothing about browser behavior. After any CORS configuration change, test in the actual browser with DevTools open, watching both the OPTIONS request and the main request in the Network tab. The /tools/http-request-builder tool is useful for testing header behavior but cannot simulate browser CORS enforcement — use it alongside browser testing, not instead of it.

Setting Access-Control-Allow-Origin in multiple places in the middleware chain causes duplicate header issues. If the Express cors() middleware sets the header and a custom middleware also sets it, some proxy configurations reject responses with duplicate headers. Remove any manual header-setting code when using the cors() middleware, and confirm no framework-level CORS settings are also active at the same time.

Maintainable CORS configuration for production APIs

Maintain a centralized list of allowed origins in an environment variable, not in code. Use a comma-separated string like ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com and split it in the cors origin function. This approach lets you add new frontend domains without a code deployment, and makes the allowlist visible in your infrastructure configuration alongside other environment-specific settings.

Log rejected CORS origins on the server side so you can detect misconfigurations before users file support tickets. The cors() middleware's origin function receives an error callback — override it to log the rejected origin, the requested URL, and the timestamp. When a new deployment of the frontend accidentally uses a different domain, the server-side logs will show rejected origins within minutes of the first affected request.

Set conservative values for Access-Control-Max-Age in development and longer values in production. In development, a 30-second cache helps you iterate quickly because preflight changes take effect almost immediately. In production, 600 seconds reduces preflight overhead without causing multi-hour delays when you need to change the CORS configuration. After deploying a CORS configuration change to production, inform the team that some users may see stale preflight behavior for up to the maxAge duration.

For APIs that serve both public and authenticated endpoints, separate the CORS configuration by route group. Public endpoints that return no sensitive data can use a wildcard origin without credentials. Authenticated endpoints must use explicit origin reflection with credentials support. This approach lets public API documentation, health check endpoints, and analytics beacons work from anywhere while keeping authenticated routes properly restricted.

Use /tools/cors-tester on ToolDock as part of your deployment verification. After deploying a new API version, check the CORS headers on the live endpoint from your frontend's origin. Confirm that Access-Control-Allow-Origin matches your frontend, that Access-Control-Allow-Headers covers your custom headers, and that the OPTIONS response returns 204. This 30-second check prevents CORS regressions from reaching users. Pair it with /tools/http-request-builder to verify the full request-response cycle, including response body parsing and status codes.

Quick fix checklist

  • Open DevTools Console and read the full CORS error message — it specifies exactly which header is missing or wrong
  • Open DevTools Network tab, filter by OPTIONS, and check whether the preflight returns 204 with correct headers
  • Confirm cors() middleware is registered before authentication middleware in Express
  • Add app.options('*', cors(options)) to handle OPTIONS explicitly at all routes
  • If using credentials: 'include', remove Access-Control-Allow-Origin: * and set the exact origin with Access-Control-Allow-Credentials: true
  • List every custom header your frontend sends in Access-Control-Allow-Headers, including Authorization and any X- headers
  • Add always flag to nginx add_header CORS directives so headers appear on error responses too
  • Test the fix in the actual browser with DevTools open, then verify with /tools/cors-tester

Related guides

Frequently asked questions

Why does my API work in curl but fail in the browser?

CORS is enforced exclusively by browsers, not by the HTTP protocol itself. curl, Postman, wget, and all non-browser HTTP clients ignore CORS headers entirely. The browser performs a CORS check after receiving the response and blocks JavaScript from reading it if the headers are missing or incorrect. The server-side access logs will show the request as successful even when the browser has blocked the response on the client side.

Can I fix CORS from the frontend without changing the server?

Not in production. The only working frontend workarounds are proxies that forward requests server-to-server, such as Next.js API routes or nginx proxy_pass configurations. These work because the browser sends the request to the same origin as the page, and the server-side proxy makes the cross-origin call where CORS does not apply. Browser extensions that disable CORS enforcement only work locally and are not a solution for end users.

What is the difference between a simple request and a preflight?

A simple request uses GET, HEAD, or POST with no custom headers and a limited Content-Type. The browser sends it directly and checks CORS headers on the response. A preflighted request uses any other method, includes a custom header like Authorization, or uses Content-Type: application/json. The browser sends an OPTIONS preflight first. The server must respond correctly to OPTIONS before the browser sends the actual request.

Why can't I use Access-Control-Allow-Origin: * with credentials?

The CORS specification explicitly prohibits combining a wildcard origin with credentials support. When credentials: 'include' is set in a fetch call or when cookies are sent, the browser requires the server to reflect the exact requesting origin so it can verify the server has explicitly opted in to receiving credentialed requests from that specific origin. A wildcard would allow any origin to make credentialed requests, which defeats the protection CORS provides.

How do I allow multiple origins in my CORS configuration?

Maintain an allowlist array and reflect the matching origin dynamically. In Express cors(), set the origin option to a function that receives the requesting origin and a callback. Check the origin against your array, and call callback(null, true) if it matches or callback(new Error('Not allowed')) if it does not. Always set Vary: Origin on the response so caches store separate versions per origin.

Why does my CORS error happen only on POST but not on GET?

GET requests without custom headers are simple requests that do not trigger a preflight. POST requests with Content-Type: application/json or an Authorization header are preflighted. If your server handles the GET correctly but the OPTIONS preflight for the POST returns 404 or 401, POST will fail while GET succeeds. Add explicit OPTIONS handling to your server and ensure CORS middleware runs before authentication middleware.

How do I configure CORS on Cloudflare?

Use Cloudflare Transform Rules to add Access-Control-Allow-Origin and related headers to matching API responses. For dynamic origin reflection or OPTIONS handling, use a Cloudflare Worker that intercepts requests, checks the Origin header against an allowlist, and adds the correct CORS headers before forwarding to the origin server. Always set Vary: Origin when using Cloudflare's caching to prevent origin-specific responses from being served to wrong origins.

Does CORS apply to WebSocket connections?

No. WebSocket connections use an upgrade handshake over HTTP that browsers do allow cross-origin without CORS headers. However, the browser does send an Origin header with the WebSocket upgrade request, and your server should validate this origin against an allowlist before accepting the connection. Rejecting unauthorized origins at the WebSocket upgrade stage is the server-side control equivalent of CORS for WebSockets.

How do I handle CORS errors in a React or Next.js app?

In Next.js, use API routes at pages/api or app/api as a server-side proxy to forward requests to external APIs. The browser calls your Next.js origin, which forwards the request server-to-server where CORS does not apply. For standalone React apps calling an external API, the API server must add the correct CORS headers. You can also configure a development proxy in vite.config.js or webpack devServer.proxy to avoid CORS during local development.

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.