All References

CORS Guide

Cross-Origin Resource Sharing explained: same-origin policy, preflight requests, headers, and how to fix common CORS errors.

What is CORS?

Browsers enforce the same-origin policy: a page at https://app.com cannot make JavaScript requests to https://api.other.com by default. An origin is the combination of scheme + host + port.

CORS (Cross-Origin Resource Sharing) is the mechanism that lets servers opt-in to allowing cross-origin requests. The server signals permission through HTTP response headers. Crucially, CORS is enforced by the browser — curl and server-to-server requests are never affected.

Without CORS, a malicious page could silently read your bank data or perform actions on your behalf on any site you're logged into.

How CORS Works

For a simple request (GET/POST with basic headers):

  1. Browser sends request with an Origin: https://app.com header.
  2. Server responds with Access-Control-Allow-Origin: https://app.com.
  3. Browser checks the header. If it matches, the response is exposed to JavaScript.

For a preflighted request (PUT/DELETE, custom headers, JSON body):

  1. Browser automatically sends an OPTIONS request first.
  2. Server responds with allowed origins, methods, and headers.
  3. If approved, browser sends the actual request.
  4. Server responds normally with CORS headers again.

Key CORS Headers

Header Direction Description
Access-Control-Allow-Origin Response Which origin(s) may access the resource. Use * for public or an exact origin for credentialed requests.
Access-Control-Allow-Methods Response (preflight) Comma-separated list of HTTP methods allowed: GET, POST, PUT, DELETE.
Access-Control-Allow-Headers Response (preflight) Which request headers are allowed (e.g., Authorization, Content-Type).
Access-Control-Allow-Credentials Response Set to true to allow cookies and auth headers. Requires explicit origin (not *).
Access-Control-Max-Age Response (preflight) Seconds to cache the preflight result, avoiding repeated OPTIONS requests. Max ~7200 in Chrome, 86400 in Firefox.
Access-Control-Expose-Headers Response Headers beyond the safe list that JS may read (e.g., X-RateLimit-Remaining).
Origin Request The origin of the requesting page, added automatically by the browser.

Simple vs Preflight Requests

A request is simple (no preflight) only if all of these are true:

Sending Content-Type: application/json, using Authorization, or using PUT/DELETE will all trigger a preflight.

Common CORS Errors and Fixes

1. Missing Access-Control-Allow-Origin

Error: "No 'Access-Control-Allow-Origin' header is present"

Fix: Add the header to your server response. For public APIs use Access-Control-Allow-Origin: *. For credentialed requests, echo back the exact requesting origin after validating it against an allowlist.

2. Wildcard + Credentials Conflict

Error: "The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'"

Fix: Replace * with the specific origin. Also add Access-Control-Allow-Credentials: true.

3. Preflight Not Handled

Error: The OPTIONS request returns 404 or 405.

Fix: Add a route or middleware that responds to OPTIONS with the appropriate CORS headers and a 204 No Content status.

Server-Side Examples

Express.js (cors middleware)

const cors = require('cors');

// Public API — allow all origins
app.use(cors());

// Credentialed API — specific origin only
app.use(cors({
  origin: 'https://app.example.com',
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400
}));

Nginx

location /api/ {
  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-Allow-Credentials' 'true' always;

  if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Max-Age' 86400;
    return 204;
  }
}

Apache (.htaccess)

Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
Header always set Access-Control-Allow-Credentials "true"

RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]

Debug CORS headers interactively with the CORS Debugger tool.

Frequently Asked Questions

Why do I get a CORS error?

CORS errors happen when your browser makes a request to a different origin (scheme + host + port) and the server's response does not include the Access-Control-Allow-Origin header matching your page's origin. The fix is always on the server side — add the appropriate CORS headers. You cannot fix a CORS error from the client-side JavaScript alone (unless you proxy through your own server).

What is a CORS preflight request?

A preflight is an automatic OPTIONS request the browser sends before a "non-simple" cross-origin request (e.g., PUT, DELETE, or any request with custom headers like Authorization or Content-Type: application/json). The browser checks whether the server permits the actual request before sending it. Your server must respond to OPTIONS with the correct Access-Control-Allow-* headers and a 2xx status.

Can I use * with Access-Control-Allow-Credentials?

No. When the client uses credentials: 'include' in fetch (to send cookies or HTTP auth), the server must respond with the exact requesting origin in Access-Control-Allow-Origin — the wildcard * is explicitly disallowed. Validate the incoming Origin request header against your allowlist and echo it back.