If you've ever seen Access to fetch at '...' has been blocked by CORS policy in your browser console, you know the frustration. CORS errors are one of the most common issues web developers encounter, yet the underlying mechanism is often misunderstood. This guide explains CORS from first principles so you can fix these errors — and understand why they exist.

What Is the Same-Origin Policy?

Browsers enforce a security rule called the Same-Origin Policy (SOP). Two URLs have the same origin if they share the same protocol, hostname, and port.

Origin Comparison
https://example.com/page1  →  https://example.com/page2     ✅ Same origin
https://example.com        →  http://example.com             ❌ Different protocol
https://example.com        →  https://api.example.com        ❌ Different hostname
https://example.com        →  https://example.com:8080       ❌ Different port

The SOP prevents a malicious site from reading data from another site. Without it, any website could make requests to your bank's API using your cookies.

What Is CORS?

CORS (Cross-Origin Resource Sharing) is a mechanism that relaxes the Same-Origin Policy. It lets a server explicitly declare which other origins are allowed to access its resources. The server communicates this through HTTP response headers.

How CORS Works

Simple Requests

For simple GET/POST requests with standard headers, the browser sends the request directly and checks the response headers:

Simple CORS Request
// Browser sends:
GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://myapp.com

// Server responds:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Content-Type: application/json

{"data": "..."}

Preflight Requests (OPTIONS)

For "non-simple" requests (custom headers, PUT/DELETE methods, JSON content type), the browser first sends an OPTIONS preflight request to ask the server for permission.

Preflight Request
// 1. Browser sends preflight:
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Content-Type, Authorization

// 2. Server responds with allowed methods/headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

// 3. Browser sends the actual request (only if preflight succeeds)

CORS Response Headers

Essential CORS Headers
Access-Control-Allow-Origin: https://myapp.com   // Which origins can access
Access-Control-Allow-Methods: GET, POST, PUT       // Allowed HTTP methods
Access-Control-Allow-Headers: Content-Type, Auth   // Allowed request headers
Access-Control-Allow-Credentials: true             // Allow cookies/auth
Access-Control-Max-Age: 86400                      // Cache preflight (seconds)
Access-Control-Expose-Headers: X-Request-Id        // Headers readable by JS

Server Configuration

Express.js (Node.js)
import cors from 'cors';

app.use(cors({
  origin: ['https://myapp.com', 'https://staging.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
}));
Nginx
location /api/ {
    add_header Access-Control-Allow-Origin "https://myapp.com" always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;

    if ($request_method = OPTIONS) {
        return 204;
    }
}
PHP
header("Access-Control-Allow-Origin: https://myapp.com");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");
header("Access-Control-Allow-Headers: Content-Type, Authorization");

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

Common CORS Mistakes

  • Using Access-Control-Allow-Origin: * with credentials: The browser will block this. You must specify the exact origin when credentials: true.
  • CORS is a browser security feature: It does NOT protect your API from server-to-server requests. Always validate authentication on every request.
  • Forgetting the always flag in Nginx: Without it, CORS headers aren't sent on error responses (4xx, 5xx).
  • Not handling OPTIONS requests: If your server returns 404 or 405 for OPTIONS, all preflight requests fail.

Try Our Free Developer Tools

Encode URLs, decode Base64, and format JSON instantly.