Client Request CORS Headers Server πŸ”’
Backend

Understanding CORS and How to Handle It

Mayur Dabhi
Mayur Dabhi
March 6, 2026
20 min read

If you've ever worked with APIs, you've almost certainly encountered the dreaded CORS error. That red console message that stops your frontend application dead in its tracks. Yet despite being one of the most common issues developers face, CORS remains widely misunderstood.

This comprehensive guide will demystify Cross-Origin Resource Sharing. You'll learn why browsers enforce this security policy, how the CORS mechanism works under the hood, and most importantly, how to properly configure your servers to handle cross-origin requests.

What You'll Learn
  • What CORS is and why browsers enforce it
  • The difference between simple and preflight requests
  • Essential CORS headers and what they do
  • How to configure CORS in Node.js, Express, Laravel, and Nginx
  • Common CORS errors and how to fix them
  • Security best practices for CORS configuration

What is CORS?

Cross-Origin Resource Sharing (CORS) is a security mechanism built into web browsers that controls how web applications running on one origin (domain) can request resources from a different origin. It's an extension of the Same-Origin Policy (SOP), which by default blocks cross-origin HTTP requests initiated by scripts.

βœ“ Same Origin (Allowed) https://app.com /page https://app.com /api/data βœ— Cross Origin (Blocked by Default) https://app.com /page βœ— https://api.com /data What defines an "Origin"? Protocol https:// + Domain example.com + Port :443 = Origin https://example.com:443

Origins consist of protocol, domain, and port. Any difference means "cross-origin"

An origin is defined by three components: the protocol (http/https), the domain (example.com), and the port (80, 443, 3000, etc.). If any of these differ between two URLs, they are considered different origins.

Same Origin Examples

https://app.com/page1 β†’ https://app.com/page2
https://app.com:443/a β†’ https://app.com/b

Cross Origin Examples

https://app.com β†’ http://app.com (protocol)
https://app.com β†’ https://api.app.com (subdomain)

Why Does CORS Exist?

CORS exists to protect users from malicious websites. Without the Same-Origin Policy and CORS, a malicious site could:

Attack Scenario Without SOP

Imagine you visit evil-site.com while logged into your bank. Without same-origin restrictions, evil-site.com's JavaScript could:

  • Fetch your account details from bank.com/api/balance
  • Initiate a transfer to the attacker's account
  • All using YOUR authenticated session cookies

CORS is the controlled relaxation of this security policy. It allows servers to explicitly declare which origins are permitted to access their resources, enabling legitimate cross-origin communication while maintaining security.

How CORS Works

When a browser makes a cross-origin request, it checks the response headers from the server to determine if the request should be allowed. The server indicates its CORS policy through specific HTTP response headers.

Simple Requests vs. Preflight Requests

CORS categorizes requests into two types: simple requests that go directly to the server, and preflight requests that require a preliminary OPTIONS check.

Simple Request Flow Browser Server 1. GET /api/data 2. Response + CORS headers Preflight Request Flow Browser Server 1. OPTIONS (preflight) 2. CORS headers 3. Actual POST request 4. Response Simple Request Conditions Methods allowed: GET, HEAD, POST Headers allowed: Accept, Accept-Language, Content-Language Content-Type (with restrictions) Content-Type values: application/x-www-form-urlencoded multipart/form-data, text/plain Preflight Required When Methods: PUT, DELETE, PATCH, etc. Custom headers: Authorization, X-Custom-Header, etc. Content-Type: application/json application/xml

Simple requests go directly to the server; preflight requests require an OPTIONS check first

Why Preflight Exists

The preflight mechanism protects servers that were built before CORS existed. These legacy servers might execute dangerous operations (DELETE, PUT) without expecting cross-origin requests. The OPTIONS check ensures the server explicitly understands and permits the cross-origin request before it's sent.

Essential CORS Headers

CORS is implemented through a set of HTTP headers. Understanding these headers is crucial for proper configuration.

Response Headers (Server β†’ Browser)

Header Description Example Value
Access-Control-Allow-Origin Specifies which origin(s) can access the resource https://app.com or *
Access-Control-Allow-Methods HTTP methods allowed for cross-origin requests GET, POST, PUT, DELETE
Access-Control-Allow-Headers Custom headers the client is allowed to send Content-Type, Authorization
Access-Control-Allow-Credentials Whether cookies/auth headers can be included true
Access-Control-Max-Age How long preflight results can be cached (seconds) 86400
Access-Control-Expose-Headers Headers the browser is allowed to access X-Request-Id, X-Rate-Limit

Request Headers (Browser β†’ Server)

Header Description Example Value
Origin The origin making the request (sent automatically) https://app.com
Access-Control-Request-Method The HTTP method of the actual request (preflight only) DELETE
Access-Control-Request-Headers Custom headers to be sent (preflight only) Content-Type, Authorization

The Classic CORS Error

This is the error message that brings developers to search engines:

Access to fetch at 'https://api.example.com/data' from origin 'https://myapp.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

This error means the server didn't include the Access-Control-Allow-Origin header in its response, or the header didn't match the requesting origin.

Important: CORS is Browser-Only

CORS is enforced by the browser, not the server. The request actually reaches the server and gets a responseβ€”the browser just refuses to let JavaScript access it. This is why:

  • Server-to-server requests work fine (no browser = no CORS)
  • Postman, cURL, and other tools ignore CORS
  • The "fix" must be implemented on the server

Implementing CORS

Let's look at how to properly configure CORS in various backend environments.

Manual CORS implementation in vanilla Node.js:

server.js
const http = require('http');

const server = http.createServer((req, res) => {
  // Set CORS headers
  res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Max-Age', '86400');

  // Handle preflight requests
  if (req.method === 'OPTIONS') {
    res.writeHead(204);
    res.end();
    return;
  }

  // Your actual route handling
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ message: 'Hello from API' }));
});

server.listen(3000);

Using the cors middleware (recommended):

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

// Simple usage - allow all origins (not for production!)
app.use(cors());

// Production-ready configuration
const corsOptions = {
  origin: ['https://myapp.com', 'https://admin.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
  credentials: true, // Allow cookies
  maxAge: 86400, // Cache preflight for 24 hours
  exposedHeaders: ['X-Request-Id'] // Headers client can access
};

app.use(cors(corsOptions));

// Dynamic origin based on request
const dynamicCors = cors({
  origin: (origin, callback) => {
    const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];
    
    // Allow requests with no origin (mobile apps, curl)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
});

app.use(dynamicCors);

Laravel includes CORS middleware since version 7. Configure in config/cors.php:

config/cors.php
<?php

return [
    /*
     * Which paths should handle CORS
     */
    'paths' => ['api/*', 'sanctum/csrf-cookie'],

    /*
     * Matches request origins against patterns
     */
    'allowed_origins' => [
        'https://myapp.com',
        'https://admin.myapp.com',
    ],
    
    // Or use patterns
    'allowed_origins_patterns' => [
        'https://*.myapp.com',
    ],

    'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],

    'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],

    'exposed_headers' => ['X-Request-Id'],

    'max_age' => 86400,

    'supports_credentials' => true,
];

Make sure the HandleCors middleware is in your app/Http/Kernel.php global middleware stack.

Add CORS headers at the server level in Nginx:

nginx.conf
server {
    listen 443 ssl;
    server_name api.example.com;

    location /api/ {
        # CORS headers
        add_header 'Access-Control-Allow-Origin' 'https://myapp.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' 86400 always;

        # Handle preflight requests
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://myapp.com';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Content-Length' 0;
            add_header 'Content-Type' 'text/plain';
            return 204;
        }

        proxy_pass http://backend;
    }
}

CORS with Credentials

When your API needs to accept cookies or authorization headers, CORS configuration becomes more restrictive.

1

Server: Enable credentials

Set Access-Control-Allow-Credentials: true in response headers.

2

Server: Specify exact origin

You cannot use * for Allow-Origin when credentials are enabled. Must specify the exact origin.

3

Client: Include credentials in request

Set credentials: 'include' in fetch or withCredentials: true in Axios.

client-with-credentials.js
// Fetch API
fetch('https://api.example.com/user', {
  method: 'GET',
  credentials: 'include', // Include cookies
  headers: {
    'Content-Type': 'application/json',
  }
});

// Axios
axios.get('https://api.example.com/user', {
  withCredentials: true // Include cookies
});

// Axios global config
axios.defaults.withCredentials = true;
Security Warning

Never use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. This is forbidden by the spec because it would allow any website to make authenticated requests to your API using the user's credentials.

Common CORS Errors and Solutions

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

Cause: Server isn't sending CORS headers.

Solution: Add the Access-Control-Allow-Origin header to your server responses. Make sure it's added for the specific routes being accessed.

res.setHeader('Access-Control-Allow-Origin', 'https://your-frontend.com');

Error: Origin not in Access-Control-Allow-Origin

Cause: The requesting origin doesn't match the allowed origin.

Solution: Verify the origin matches exactly (including protocol, domain, and port). Check for trailing slashes or www vs non-www differences.

// Wrong: port mismatch
'https://localhost:3000' !== 'https://localhost:3001'

// Wrong: trailing slash
'https://app.com/' !== 'https://app.com'

// Wrong: www mismatch  
'https://www.app.com' !== 'https://app.com'

Error: Request header not allowed

Cause: You're sending a custom header that isn't in Access-Control-Allow-Headers.

Solution: Add the header to your CORS configuration.

res.setHeader('Access-Control-Allow-Headers', 
  'Content-Type, Authorization, X-Custom-Header');

Error: Method not allowed

Cause: The HTTP method (PUT, DELETE, etc.) isn't in Access-Control-Allow-Methods.

Solution: Add the method to your allowed methods list.

res.setHeader('Access-Control-Allow-Methods', 
  'GET, POST, PUT, DELETE, PATCH, OPTIONS');

Error: Wildcard (*) with credentials

Cause: Using * for Allow-Origin while also requiring credentials.

Solution: Dynamically set the origin based on the request's Origin header.

const allowedOrigins = ['https://app.com', 'https://admin.app.com'];
const origin = req.headers.origin;

if (allowedOrigins.includes(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
  res.setHeader('Access-Control-Allow-Credentials', 'true');
}

Security Best Practices

CORS configuration is a security decision. Here's how to do it right:

CORS Security Checklist

  • Never use * in production β€” Always specify exact allowed origins
  • Validate the Origin header β€” Check against a whitelist, don't blindly echo it back
  • Be specific with methods β€” Only allow the HTTP methods your API actually uses
  • Limit allowed headers β€” Only permit headers your frontend actually sends
  • Set appropriate Max-Age β€” Cache preflight results to reduce OPTIONS requests
  • Review credentials setting β€” Only enable if you need cookies/auth headers
  • Don't bypass CORS in production β€” Proxy solutions should only be for development
  • Monitor and log CORS failures β€” They might indicate an attack or misconfiguration
Don't Reflect the Origin Header

A dangerous anti-pattern is setting Access-Control-Allow-Origin to whatever the request's Origin header contains. This effectively disables CORS protection entirely. Always validate against a whitelist of allowed origins.

Development Workarounds

During development, you might need to bypass CORS temporarily. Here are safe approaches:

Proxy Server

Configure your dev server (Vite, Webpack, CRA) to proxy API requests. The proxy runs same-origin, so no CORS issues.

Dev-only CORS

Enable permissive CORS only in development mode. Use environment variables to control the configuration.

Browser Extensions

Extensions like "CORS Unblock" disable CORS for testing. Never use in production browsing!

Chrome Flags

Launch Chrome with --disable-web-security. Only for isolated testing environments.

vite.config.js - Proxy Example
export default {
  server: {
    proxy: {
      // Proxy /api requests to backend
      '/api': {
        target: 'https://api.example.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

Debugging CORS Issues

When troubleshooting CORS, your browser's developer tools are your best friend:

1

Check the Network Tab

Look at the actual request and response headers. Verify the Origin, Access-Control-Allow-Origin, and other CORS headers are present and correct.

2

Look for Preflight (OPTIONS)

If your request triggers a preflight, check if the OPTIONS request succeeds and returns proper CORS headers. A failing preflight blocks the actual request.

3

Test with cURL

Use cURL to verify the server returns correct headers. This eliminates browser-specific issues from troubleshooting.

debug-cors.sh
# Check simple request headers
curl -I -H "Origin: https://myapp.com" \
  https://api.example.com/data

# Simulate preflight request
curl -X OPTIONS \
  -H "Origin: https://myapp.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization" \
  -I https://api.example.com/data

# Look for these in response:
# Access-Control-Allow-Origin: https://myapp.com
# Access-Control-Allow-Methods: POST, GET, ...
# Access-Control-Allow-Headers: Content-Type, Authorization

Key Takeaways

Summary

  • CORS is browser-enforced β€” The server receives and processes the request; the browser decides whether to expose the response to JavaScript
  • Same-origin = same protocol + domain + port β€” Any difference triggers cross-origin policies
  • Simple vs preflight β€” Complex requests trigger an OPTIONS preflight check
  • Server must opt-in β€” CORS headers explicitly permit cross-origin access
  • Be specific in production β€” Never use wildcard (*) with real applications
  • Credentials require exact origin β€” Can't use * when cookies are involved
  • Debug with network tools β€” Check headers, look for preflight failures

Understanding CORS transforms it from a frustrating obstacle into a manageable security feature. With proper server configuration, your APIs can safely serve web applications across different origins while maintaining the security protections that keep users safe.

CORS API Security Node.js Express Laravel Backend
Mayur Dabhi

Mayur Dabhi

Full-stack developer specializing in Laravel, React, and Node.js. Writing about web development, APIs, and best practices.