CORS Explained: Cross-Origin Resource Sharing

Easy

CORS is a critical security mechanism. Understanding how it works is essential for building web applications that make cross-origin requests.

Asked In

Quick Decision Guide

Quick Fix Guide:

Common Error: "CORS policy blocked" when frontend (localhost:3000) calls backend (localhost:8000).

Solution: Configure server to send CORS headers: - Access-Control-Allow-Origin: Set to your frontend domain (never * in production) - Access-Control-Allow-Methods: GET, POST, PUT, DELETE - Access-Control-Allow-Headers: Content-Type, Authorization

Preflight Requests: Browser sends OPTIONS request first for PUT/DELETE/custom headers. Server must handle OPTIONS and return CORS headers.

Express.js Example: Use cors middleware: app.use(cors({ origin: 'http://localhost:3000' }))

Security Tip: Never use Access-Control-Allow-Origin: * with credentials. Always specify exact origin in production.

How CORS Works

Request Flow

Simple Request (No Preflight)

// Frontend makes request
fetch('https://api.com/data');

// Browser checks:
// 1. Is origin different? Yes
// 2. Is it a simple request? Yes
// 3. Browser sends request with Origin header
// 4. Server responds with CORS headers
// 5. Browser checks headers, allows/denies

Preflight Request

For "non-simple" requests, browser sends OPTIONS request first:

// Frontend makes request
fetch('https://api.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token',
  },
});

// Browser sends preflight:
OPTIONS https://api.com/data
Origin: https://app.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

// Server responds:
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: POST, GET
Access-Control-Allow-Headers: content-type, authorization

// Browser checks response, then sends actual request

Simple vs Preflight Requests

Simple Requests (No Preflight)

Methods: GET, HEAD, POST
Headers: Only simple headers (Content-Type: text/plain, application/x-www-form-urlencoded, multipart/form-data)
No custom headers

Preflight Required

Methods: PUT, DELETE, PATCH
Custom headers: Authorization, X-Custom-Header
Content-Type: application/json

CORS Headers Explained

Essential CORS Headers

Access-Control-Allow-Origin

// Allow specific origin
Access-Control-Allow-Origin: https://app.com

// Allow all origins (NOT recommended for production)
Access-Control-Allow-Origin: *

// Allow multiple origins (must check in code)
if (allowedOrigins.includes(req.headers.origin)) {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
}

Access-Control-Allow-Methods

// Allow specific methods
Access-Control-Allow-Methods: GET, POST, PUT, DELETE

// Allow all methods
Access-Control-Allow-Methods: *

Access-Control-Allow-Headers

// Allow specific headers
Access-Control-Allow-Headers: Content-Type, Authorization

// Allow all headers
Access-Control-Allow-Headers: *

Access-Control-Allow-Credentials

// Allow cookies/credentials
Access-Control-Allow-Credentials: true

// Frontend must also set:
fetch(url, {
  credentials: 'include',
});

Note: Cannot use * with credentials

Common CORS Errors

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

Problem: Server doesn't send CORS headers

Solution:

// Express.js
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*');
  next();
});

Error: "Preflight request doesn't pass"

Problem: Preflight OPTIONS request fails

Solution:

// Handle OPTIONS request
app.options('*', (req, res) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.sendStatus(200);
});

Error: "Credentials flag is true but 'Access-Control-Allow-Credentials' is not 'true'"

Problem: Using credentials but server doesn't allow

Solution:

// Server
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Origin', 'https://app.com'); // Must be specific, not *

// Client
fetch(url, {
  credentials: 'include',
});

Error: "Request header field is not allowed"

Problem: Custom header not allowed

Solution:

// Server must include header in Allow-Headers
res.header('Access-Control-Allow-Headers', 'X-Custom-Header');

Server-Side Implementation

Express.js Example

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

// CORS middleware
const allowedOrigins = [
  'https://app.com',
  'https://www.app.com',
  'http://localhost:3000',
];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
  }
  
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Max-Age', '86400'); // 24 hours
  
  // Handle preflight
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  
  next();
});

app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello CORS!' });
});

Using cors Package

const cors = require('cors');

app.use(cors({
  origin: ['https://app.com', 'http://localhost:3000'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

Next.js API Routes

// pages/api/data.js
export default function handler(req, res) {
  res.setHeader('Access-Control-Allow-Origin', 'https://app.com');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  
  if (req.method === 'OPTIONS') {
    return res.status(200).end();
  }
  
  res.json({ data: 'Hello' });
}

Best Practices

Security Considerations

1. Don't Use Wildcard (*) in Production

// ❌ BAD
Access-Control-Allow-Origin: *

// ✅ GOOD
Access-Control-Allow-Origin: https://app.com

2. Whitelist Specific Origins

const allowedOrigins = [
  'https://app.com',
  'https://www.app.com',
];

if (allowedOrigins.includes(req.headers.origin)) {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
}

3. Use Environment Variables

const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];

4. Set Appropriate Max-Age

// Cache preflight for 24 hours
Access-Control-Max-Age: 86400

Common Patterns

Pattern 1: Public API

// Allow all origins, no credentials
app.use(cors({
  origin: '*',
  credentials: false,
}));

Pattern 2: Authenticated API

// Specific origins, with credentials
app.use(cors({
  origin: ['https://app.com'],
  credentials: true,
}));

Pattern 3: Development Only

if (process.env.NODE_ENV === 'development') {
  app.use(cors({
    origin: 'http://localhost:3000',
  }));
}

Debugging CORS

// Log CORS requests
app.use((req, res, next) => {
  console.log('Origin:', req.headers.origin);
  console.log('Method:', req.method);
  console.log('Headers:', req.headers['access-control-request-headers']);
  next();
});