Skip to main content

Command Palette

Search for a command to run...

CORS Configuration: Cross-Origin Requests

Learn: CORS Configuration: Cross-Origin Requests

Updated
7 min readView as Markdown
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

CORS Configuration: Cross-Origin Requests, Preflight, and Headers

Problem

When building modern web applications, frontend code running on one domain often needs to request resources from APIs on different domains. Browsers enforce the Same-Origin Policy for security, blocking these cross-origin requests by default. This creates friction in development and production environments where:

  • Frontend (example.com) cannot fetch from API (api.example.com)
  • Single-page applications cannot communicate with third-party services
  • Mobile apps wrapping web content face authentication issues
  • Microservices architectures struggle with domain separation

Without proper CORS (Cross-Origin Resource Sharing) configuration, legitimate requests fail silently or throw cryptic errors, leaving developers confused about what went wrong.


Solution

CORS is a browser-based security mechanism that allows servers to explicitly grant cross-origin access through HTTP headers. The solution involves:

  1. Understanding the Same-Origin Policy: Origins are defined by protocol, domain, and port (https://api.example.com:443 ≠ https://api.example.com:8443)

  2. Implementing Preflight Requests: For complex requests, browsers automatically send OPTIONS requests to verify server permissions before sending the actual request

  3. Configuring Response Headers: Servers respond with specific headers that tell browsers which origins, methods, and headers are allowed

  4. Handling Credentials: Special configuration is needed when requests include cookies or authentication headers

  5. Server-Side Implementation: Configure CORS at the application level, not just at the reverse proxy


Code

Node.js/Express

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

// Basic CORS - Allow all origins
app.use(cors());

// Detailed CORS configuration
const corsOptions = {
  origin: function (origin, callback) {
    const allowedOrigins = [
      'https://example.com',
      'https://app.example.com',
      'http://localhost:3000',
      'http://localhost:5173'
    ];

    // Allow requests with no origin (like mobile apps or curl requests)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },

  credentials: true, // Allow cookies/auth headers
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
  exposedHeaders: ['X-Total-Count', 'X-Page-Number'], // Headers client can access
  maxAge: 86400 // Preflight cache duration (24 hours)
};

app.use(cors(corsOptions));

// Handle preflight requests explicitly
app.options('*', cors(corsOptions));

// Or for specific routes
app.options('/api/users', cors(corsOptions));
app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

// Manual CORS headers (if not using middleware)
app.get('/api/data', (req, res) => {
  res.header('Access-Control-Allow-Origin', 'https://example.com');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Max-Age', '86400');
  res.json({ data: 'sensitive' });
});

app.listen(3001, () => console.log('API running on port 3001'));

Python/Flask

from flask import Flask, jsonify
from flask_cors import CORS, cross_origin

app = Flask(__name__)

# Basic CORS - Allow all origins
CORS(app)

# Detailed CORS configuration
cors_config = {
    "origins": [
        "https://example.com",
        "https://app.example.com",
        "http://localhost:3000"
    ],
    "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
    "allow_headers": ["Content-Type", "Authorization", "X-Requested-With"],
    "expose_headers": ["X-Total-Count", "X-Page-Number"],
    "supports_credentials": True,
    "max_age": 86400
}

CORS(app, resources={
    r"/api/*": cors_config
})

# Decorator approach for specific routes
@app.route('/api/users', methods=['GET', 'POST', 'OPTIONS'])
@cross_origin(
    origins=["https://example.com", "http://localhost:3000"],
    methods=["GET", "POST"],
    allow_headers=["Content-Type", "Authorization"],
    supports_credentials=True,
    max_age=86400
)
def get_users():
    return jsonify({"users": []})

# Manual headers
@app.route('/api/data')
def get_data():
    response = jsonify({"data": "sensitive"})
    response.headers['Access-Control-Allow-Origin'] = 'https://example.com'
    response.headers['Access-Control-Allow-Credentials'] = 'true'
    response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
    response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
    response.headers['Access-Control-Max-Age'] = '86400'
    return response

if __name__ == '__main__':
    app.run(port=3001, debug=True)

Python/Django

# settings.py
INSTALLED_APPS = [
    # ...
    'corsheaders',
]

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

CORS_ALLOWED_ORIGINS = [
    "https://example.com",
    "https://app.example.com",
    "http://localhost:3000",
    "http://localhost:5173",
]

CORS_ALLOW_CREDENTIALS = True

CORS_ALLOW_METHODS = [
    "DELETE",
    "GET",
    "OPTIONS",
    "PATCH",
    "POST",
    "PUT",
]

CORS_ALLOW_HEADERS = [
    "accept",
    "accept-encoding",
    "authorization",
    "content-type",
    "dnt",
    "origin",
    "user-agent",
    "x-csrftoken",
    "x-requested-with",
]

CORS_EXPOSE_HEADERS = [
    "x-total-count",
    "x-page-number",
]

CORS_MAX_AGE = 86400

# views.py
from django.http import JsonResponse
from corsheaders.decorators import ensure_csrf_cookie

@ensure_csrf_cookie
def get_users(request):
    if request.method == 'OPTIONS':
        response = JsonResponse({})
    else:
        response = JsonResponse({"users": []})

    response['Access-Control-Allow-Origin'] = 'https://example.com'
    response['Access-Control-Allow-Credentials'] = 'true'
    return response

Java/Spring Boot

// Configuration class
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins(
                "https://example.com",
                "https://app.example.com",
                "http://localhost:3000"
            )
            .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
            .allowedHeaders("*")
            .exposedHeaders("X-Total-Count", "X-Page-Number")
            .allowCredentials(true)
            .maxAge(86400);
    }
}

// Controller with annotation
import org.springframework.web.bind.annotation.*;
import org.springframework.web.cors.CorsConfiguration;

@RestController
@RequestMapping("/api")
@CrossOrigin(
    origins = {"https://example.com", "http://localhost:3000"},
    methods = {RequestMethod.GET, RequestMethod.POST},
    allowedHeaders = {"Content-Type", "Authorization"},
    exposedHeaders = {"X-Total-Count"},
    allowCredentials = "true",
    maxAge = 86400
)
public class UserController {

    @GetMapping("/users")
    public ResponseEntity<?> getUsers() {
        return ResponseEntity.ok(new Users());
    }
}

// application.yml
spring:
  web:
    cors:
      allowed-origins: https://example.com,http://localhost:3000
      allowed-methods: GET,POST,PUT,DELETE,PATCH,OPTIONS
      allowed-headers: "*"
      exposed-headers: X-Total-Count,X-Page-Number
      allow-credentials: true
      max-age: 86400

Nginx (Reverse Proxy)

# nginx.conf
upstream api_backend {
    server localhost:3001;
}

server {
    listen 80;
    server_name api.example.com;

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

        # Regular requests
        proxy_pass http://api_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # CORS headers
        add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Expose-Headers' 'X-Total-Count, X-Page-Number' always;
    }
}

Frontend/JavaScript

// Simple fetch with CORS
fetch('https://api.example.com/users', {
  method: 'GET',
  credentials: 'include', // Include cookies
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123'
  }
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('CORS error:', error));

// Axios with CORS
import axios from 'axios';

const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  withCredentials: true, // Include cookies
  headers: {
    'Content-Type': 'application/json'
  }
});

apiClient.get('/users')
  .then(response => console.log(response.data))
  .catch(error => console.error('CORS error:', error));

// React with custom hook
import { useEffect, useState } from 'react';

function useApi(url) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch(url, {
      credentials: 'include',
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('token')}`
      }
    })
    .then(res => res.json())
    .then(data => setData(data))
    .catch(err => setError(err));
  }, [url]);

  return { data, error };
}

Tips

1. Understand Preflight Requests

  • Browsers automatically send OPTIONS requests for "complex" requests (POST, PUT, DELETE, or requests with custom headers)
  • Preflight results are cached based on Access-Control-Max-Age header
  • Set reasonable cache times (3600-86400 seconds) to reduce preflight overhead
  • Simple requests (GET, HEAD, POST with standard headers) don't trigger preflight

2. Credentials and Authentication

  • Set credentials: 'include' on frontend AND Access-Control-Allow-Credentials: true on backend
  • When credentials are enabled, Access-Control-Allow-Origin cannot be * (must be specific origin)
  • Cookies are only sent if both sides explicitly allow it
  • Authorization headers require explicit allowance in Access-Control-Allow-Headers

3. Security Best Practices

  • Never use Access-Control-Allow-Origin: * with credentials: true
  • Whitelist specific origins instead of allowing all
  • Use environment variables for allowed origins (different per environment)
  • Validate origin on backend, don't blindly echo the Origin header
  • Restrict HTTP methods to what's actually needed
  • Only expose headers that clients need via Access-Control-Expose-Headers

4. Common Mistakes

  • Forgetting to handle OPTIONS requests (preflight fails silently)
  • Using * for origins when credentials are needed
  • Not setting credentials: 'include' on frontend when backend requires it
  • Exposing sensitive headers unnecessarily
  • Configuring CORS only at reverse proxy, not at application level
  • Not testing with actual cross-origin requests (localhost:3000 → localhost:3001)

5. Debugging CORS Issues

// Check browser console for CORS errors
// Look for: "Access to XMLHttpRequest at 'X' from origin 'Y' has been blocked by CORS policy"

// Use curl to test backend CORS headers
curl -H "Origin: https://example.com" \
     -H "Access-Control-Request-Method: POST" \
     -H "Access-Control-Request-Headers: Content-Type" \
     -X OPTIONS https://api.example.com/users -v

// Check response headers for CORS configuration
curl -H "Origin: https://example.com" \
     https://api.example.com/users -v

6. Environment-Specific Configuration

// Development
const allowedOrigins = [
  'http://localhost:3000',
  'http://localhost:5173',
  'http://127.0.0.1:3000'
];

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

// Dynamic based on NODE_ENV
const corsOrigins = process.env.NODE_ENV === 'production' 
  ? productionOrigins 
  : developmentOrigins;

7. Testing CORS Configuration

// Test script
async function testCors() {
  const origins = [
    'https://example.com',
    'https://app.example.com',
    'https://unauthorized.com'
  ];

  for (const origin of origins) {
    try {
      const response = await fetch('https://api.example.com/users', {
        headers: { 'Origin': origin }
      });
      console.log(`${origin}: ${response.status}`);
    } catch (e) {
      console.log(`${origin}: BLOCKED`);
    }
  }
}

8. Performance Optimization

  • Increase Access-Control-Max-Age to cache preflight responses (reduces requests)
  • Use simple requests when possible (GET, HEAD, POST with standard headers)
  • Combine multiple API calls to reduce preflight overhead
  • Consider using a single endpoint for multiple operations
  • Monitor preflight requests in production (they add latency)

9. Wildcard Patterns

// Some frameworks support patterns
const corsOptions = {
  origin: /\.example\.com$/, // Matches *.example.com
  // or
  origin: function(origin, callback) {
    if (origin && origin.endsWith('.example.com')) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed'));
    }
  }
};

10. CORS vs. JSONP

  • CORS is the modern standard (use this)
  • JSONP is legacy workaround (avoid unless supporting very old browsers)
  • CORS works with all HTTP methods; JSONP only works with GET
  • CORS supports credentials; JSONP doesn't

Summary

CORS is essential for modern web development. Implement it by:

  1. Configuring allowed origins (whitelist specific domains)
  2. Handling preflight OPTIONS requests
  3. Setting appropriate response headers
  4. Managing credentials carefully
  5. Testing thoroughly across environments

Proper CORS configuration balances security with functionality, enabling legitimate cross-origin communication while protecting against unauthorized access.