DEV Community

Weather Clock Dash
Weather Clock Dash

Posted on

CORS Explained: Why It's a Browser Problem (and How to Fix It on the Server)

CORS (Cross-Origin Resource Sharing) is responsible for more confused developer Stack Overflow questions than almost any other browser feature. The error messages are cryptic, the behavior is counterintuitive, and the fix is almost always on the server — not the browser.

This guide covers how CORS actually works, why you're seeing these errors, and how to configure it correctly.

What Is a Cross-Origin Request?

Two URLs share the same "origin" if they have the same protocol, host, and port:

https://app.example.com      <- origin A
https://api.example.com      <- different origin (different subdomain)
http://app.example.com       <- different origin (different protocol)
https://app.example.com:8080 <- different origin (different port)
Enter fullscreen mode Exit fullscreen mode

When JavaScript in a browser makes a fetch/XHR to a different origin, the browser enforces CORS rules. This is a browser-side security feature — the request may well succeed on the server, but the browser blocks the JavaScript from reading the response.

The Two Types of CORS Requests

Simple Requests

A "simple request" goes through in one round trip. Criteria:

  • Method is GET, POST, or HEAD
  • Content-Type is application/x-www-form-urlencoded, multipart/form-data, or text/plain
  • No custom headers

The browser sends the request and checks Access-Control-Allow-Origin in the response:

GET /api/data HTTP/1.1
Origin: https://app.example.com

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Enter fullscreen mode Exit fullscreen mode

If the header matches (or is *), the browser lets JavaScript read the response.

Preflighted Requests

For anything more complex — PUT/DELETE, Content-Type: application/json, custom headers — the browser first sends an OPTIONS preflight:

OPTIONS /api/data HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
Enter fullscreen mode Exit fullscreen mode

The server must respond to the preflight before the actual request proceeds:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Enter fullscreen mode Exit fullscreen mode

If the preflight response is missing or wrong, the actual request never fires.

Common CORS Error Messages

"No 'Access-Control-Allow-Origin' header"

The server didn't send the CORS header at all. The request succeeded server-side — you just can't read it from JavaScript.

Fix: Add Access-Control-Allow-Origin to your server response.

"value ... must not be the wildcard '*' when the request's credentials mode is 'include'"

You're sending cookies/auth with credentials: 'include', but the server responds with Access-Control-Allow-Origin: *.

Fix: Set a specific origin AND add Access-Control-Allow-Credentials: true.

// Wrong:
res.setHeader('Access-Control-Allow-Origin', '*');

// Right (when using credentials):
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
Enter fullscreen mode Exit fullscreen mode

"Method DELETE is not allowed by Access-Control-Allow-Methods"

Your preflight doesn't include the method you're using. Add it to Access-Control-Allow-Methods.

"Request header field Authorization is not allowed by Access-Control-Request-Headers"

Add Authorization to your Access-Control-Allow-Headers preflight response.

Server-Side Configuration

Express (Node.js)

const cors = require('cors');

// Public API — any origin, no credentials
app.use(cors());

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

// Dynamic origin validation (e.g. wildcard subdomain)
app.use(cors({
  origin: (origin, callback) => {
    const allowed = ['https://app.example.com'];
    if (!origin || allowed.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true
}));
Enter fullscreen mode Exit fullscreen mode

Nginx

location /api {
    add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;

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

    proxy_pass http://backend;
}
Enter fullscreen mode Exit fullscreen mode

The always flag is critical — without it, Nginx only adds CORS headers on 200 responses, not error responses. You'll get CORS errors on 500s and 404s.

Django

# pip install django-cors-headers
# settings.py
INSTALLED_APPS = ['corsheaders', ...]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]

CORS_ALLOWED_ORIGINS = ['https://app.example.com']
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = ['accept', 'authorization', 'content-type']
Enter fullscreen mode Exit fullscreen mode

Credentials and Cookies

Client side:

fetch('https://api.example.com/data', {
  credentials: 'include'  // sends cookies
});

// axios
axios.get('https://api.example.com/data', { withCredentials: true });
Enter fullscreen mode Exit fullscreen mode

Both conditions required on the server:

  1. Access-Control-Allow-Origin = specific origin (never *)
  2. Access-Control-Allow-Credentials: true

Preflight Caching

Every preflight adds a round trip. Cache them with Access-Control-Max-Age:

Access-Control-Max-Age: 86400  # 1 day
Enter fullscreen mode Exit fullscreen mode

Firefox caches up to 24 hours; Chrome up to 2 hours. Set to 86400 and let browsers apply their caps.

Development: Use a Proxy Instead

Avoid CORS entirely in development by proxying API calls through your dev server:

Vite:

export default {
  server: {
    proxy: {
      '/api': { target: 'http://localhost:3001', changeOrigin: true }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Create React App:

{ "proxy": "http://localhost:3001" }
Enter fullscreen mode Exit fullscreen mode

Same origin — no CORS required.

Debugging CORS in Firefox DevTools

Firefox gives the most detailed CORS error messages:

  1. Open Network tab
  2. Click the failed request
  3. Firefox often shows the exact missing header in the response section
  4. Preflight (OPTIONS) requests appear explicitly, labeled separately

The Security tab on a request also shows which CORS policy blocked access.

Common Gotchas

Wildcard subdomain matching doesn't exist. *.example.com is not valid in Access-Control-Allow-Origin. Dynamically validate and echo the specific origin:

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && /^https:\/\/[\w-]+\.example\.com$/.test(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  next();
});
Enter fullscreen mode Exit fullscreen mode

CORS errors on 4xx/5xx responses: If your middleware only adds CORS headers on success, preflight failures return CORS errors instead of the actual error. Use always in Nginx, or ensure your error handlers also set the headers.

null origin: Locally-opened HTML files send Origin: null. Never add null to your allowed origins — any sandboxed iframe can exploit this.

CORS is not an API security feature. Server-to-server requests and curl bypass CORS entirely. It's a browser-enforced policy for JavaScript — you still need authentication for your API.

Understanding CORS becomes straightforward once you internalize that the browser is the enforcer, not the server, and the fix is always a header the server needs to send.

Top comments (0)