← Back to Blog

Fix CORS "No Access-Control-Allow-Origin" Error: Every Platform

Quick Fix for Panicking Engineers

Your frontend is getting blocked by CORS. Add these headers to your API server response:

# Nginx (add to server or location block)
add_header 'Access-Control-Allow-Origin' 'https://yourfrontend.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
if ($request_method = 'OPTIONS') { return 204; }

# Node.js/Express (one liner)
app.use(require('cors')({ origin: 'https://yourfrontend.com', credentials: true }));

# Apache (.htaccess)
Header always set Access-Control-Allow-Origin "https://yourfrontend.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
RewriteRule ^(.*)$ $1 [R=204,L] [env=!REQUEST_METHOD:OPTIONS]

Replace https://yourfrontend.com with your actual frontend origin. Never use * if you send cookies or Authorization headers.

The browser console shows Access to XMLHttpRequest at 'https://api.example.com' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Your API works in Postman. It works in curl. It works from your backend. It only fails from the browser. That is because CORS is a browser-enforced security policy, not a server-side restriction. The server must explicitly permit cross-origin requests. This guide covers the exact configuration for every major platform.

Why CORS Exists

Without CORS, any website could make requests to any other website using the visitor's cookies. A malicious page could call your banking API, read the response, and exfiltrate data. The Same-Origin Policy prevents this by default. CORS is the mechanism that allows controlled exceptions to the Same-Origin Policy.

An "origin" is the combination of scheme + host + port. https://app.example.com and https://api.example.com are different origins. http://localhost:3000 and http://localhost:8080 are different origins. http://example.com and https://example.com are different origins.

The Two Types of CORS Requests

Simple Requests (No Preflight)

A request is "simple" if it meets all of these conditions:

  • Method is GET, HEAD, or POST
  • Content-Type is application/x-www-form-urlencoded, multipart/form-data, or text/plain
  • No custom headers (no Authorization, no X-Custom-Header)

For simple requests, the browser sends the request directly and checks the response for Access-Control-Allow-Origin.

Preflighted Requests

Any request that does not meet the "simple" criteria triggers a preflight. The browser sends an OPTIONS request first, asking: "Is it okay for me to send a POST with Content-Type: application/json and an Authorization header from origin X?" If the server responds with the correct CORS headers, the browser sends the actual request.

# What a preflight OPTIONS request looks like:
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

# What the server must respond with:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Fix for Nginx

# /etc/nginx/sites-available/api.conf
server {
    listen 443 ssl http2;
    server_name api.example.com;

    # CORS headers for all responses
    # Use $http_origin to dynamically reflect the allowed origin
    set $cors_origin "";
    if ($http_origin ~* "^https://(app\.example\.com|staging\.example\.com)$") {
        set $cors_origin $http_origin;
    }

    add_header 'Access-Control-Allow-Origin' $cors_origin always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, Accept, Origin' always;
    add_header 'Access-Control-Expose-Headers' 'Content-Length, X-Request-Id' always;
    add_header 'Access-Control-Max-Age' '86400' always;

    # Handle preflight requests
    if ($request_method = 'OPTIONS') {
        return 204;
    }

    location / {
        proxy_pass http://backend;
    }
}

Critical detail: The always parameter is required. Without it, Nginx only adds headers on 2xx responses. If your backend returns a 4xx or 5xx error, the CORS headers will be missing and the browser will show a CORS error instead of the actual error message. This makes debugging impossible.

Nginx: Why add_header Disappears in Nested Blocks

# WARNING: add_header directives in a location block OVERRIDE
# all add_header directives from the parent server block.
# This is an Nginx gotcha that causes intermittent CORS failures.

server {
    add_header 'Access-Control-Allow-Origin' '*' always;  # Set at server level

    location /api {
        add_header 'X-Custom' 'value';  # This REMOVES the CORS header above!
        proxy_pass http://backend;
    }
}

# Fix: repeat all headers in every location block, or use the
# headers-more-nginx-module which does not have this inheritance issue.

Fix for Apache

# .htaccess or VirtualHost config
# Enable mod_headers and mod_rewrite
Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With"
Header always set Access-Control-Allow-Credentials "true"
Header always set Access-Control-Max-Age "86400"

# Handle preflight OPTIONS requests
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]

Apache: Dynamic Origin Allowlisting

# Allow multiple origins dynamically
SetEnvIf Origin "^https://(app\.example\.com|staging\.example\.com)$" CORS_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%{CORS_ORIGIN}e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN

Fix for Node.js / Express

Using the cors Package (Recommended)

const cors = require('cors');

// Simple: allow one origin
app.use(cors({
  origin: 'https://app.example.com',
  credentials: true,
}));

// Multiple origins
app.use(cors({
  origin: ['https://app.example.com', 'https://staging.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
  exposedHeaders: ['Content-Length', 'X-Request-Id'],
  credentials: true,
  maxAge: 86400,
}));

// Dynamic origin validation (e.g., any subdomain)
app.use(cors({
  origin: function (origin, callback) {
    // Allow requests with no origin (mobile apps, curl, server-to-server)
    if (!origin) return callback(null, true);

    if (origin.endsWith('.example.com') || origin === 'https://example.com') {
      return callback(null, true);
    }
    callback(new Error('Not allowed by CORS'));
  },
  credentials: true,
}));

Manual Implementation (No Dependencies)

app.use((req, res, next) => {
  const allowedOrigins = ['https://app.example.com', 'https://staging.example.com'];
  const origin = req.headers.origin;

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

  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  res.setHeader('Access-Control-Max-Age', '86400');

  if (req.method === 'OPTIONS') {
    return res.status(204).end();
  }

  next();
});

Fix for Django

# Install django-cors-headers
pip install django-cors-headers

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

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # MUST be before CommonMiddleware
    'django.middleware.common.CommonMiddleware',
    ...
]

# Allow specific origins
CORS_ALLOWED_ORIGINS = [
    'https://app.example.com',
    'https://staging.example.com',
]

# Or allow patterns (e.g., all subdomains)
CORS_ALLOWED_ORIGIN_REGEXES = [
    r'^https://.*\.example\.com$',
]

CORS_ALLOW_CREDENTIALS = True

CORS_ALLOW_HEADERS = [
    'accept',
    'authorization',
    'content-type',
    'origin',
    'x-requested-with',
]

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

# Cache preflight responses for 24 hours
CORS_PREFLIGHT_MAX_AGE = 86400

Test Your API Headers

Use SecureBin's HTTP Status Code reference to understand response codes your API returns alongside CORS headers.

HTTP Status Codes

Fix for Flask

# Install flask-cors
pip install flask-cors

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)

# Allow specific origins
CORS(app, origins=['https://app.example.com'],
     supports_credentials=True,
     allow_headers=['Content-Type', 'Authorization'],
     methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])

# Or configure per-route
from flask_cors import cross_origin

@app.route('/api/data', methods=['GET', 'OPTIONS'])
@cross_origin(origins='https://app.example.com', supports_credentials=True)
def get_data():
    return jsonify({'data': 'value'})

Fix for AWS API Gateway

REST API (v1)

# Enable CORS via the AWS Console:
# 1. Select your resource
# 2. Actions > Enable CORS
# 3. Enter your allowed origin (not *)
# 4. Deploy the API

# Or via CLI:
aws apigateway put-method \
  --rest-api-id abc123 \
  --resource-id xyz789 \
  --http-method OPTIONS \
  --authorization-type NONE

aws apigateway put-integration \
  --rest-api-id abc123 \
  --resource-id xyz789 \
  --http-method OPTIONS \
  --type MOCK \
  --request-templates '{"application/json": "{\"statusCode\": 200}"}'

aws apigateway put-method-response \
  --rest-api-id abc123 \
  --resource-id xyz789 \
  --http-method OPTIONS \
  --status-code 200 \
  --response-parameters '{
    "method.response.header.Access-Control-Allow-Headers": false,
    "method.response.header.Access-Control-Allow-Methods": false,
    "method.response.header.Access-Control-Allow-Origin": false
  }'

aws apigateway put-integration-response \
  --rest-api-id abc123 \
  --resource-id xyz789 \
  --http-method OPTIONS \
  --status-code 200 \
  --response-parameters '{
    "method.response.header.Access-Control-Allow-Headers": "'Content-Type,Authorization'",
    "method.response.header.Access-Control-Allow-Methods": "'GET,POST,PUT,DELETE,OPTIONS'",
    "method.response.header.Access-Control-Allow-Origin": "'https://app.example.com'"
  }'

HTTP API (v2)

# HTTP API v2 has built-in CORS configuration
aws apigatewayv2 update-api \
  --api-id abc123 \
  --cors-configuration '{
    "AllowOrigins": ["https://app.example.com"],
    "AllowMethods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    "AllowHeaders": ["Content-Type", "Authorization"],
    "AllowCredentials": true,
    "MaxAge": 86400
  }'

Fix for Cloudflare Workers

// Cloudflare Worker that adds CORS headers
const ALLOWED_ORIGINS = [
  'https://app.example.com',
  'https://staging.example.com',
];

function handleOptions(request) {
  const origin = request.headers.get('Origin');
  if (ALLOWED_ORIGINS.includes(origin)) {
    return new Response(null, {
      status: 204,
      headers: {
        'Access-Control-Allow-Origin': origin,
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization',
        'Access-Control-Allow-Credentials': 'true',
        'Access-Control-Max-Age': '86400',
      },
    });
  }
  return new Response(null, { status: 403 });
}

async function handleRequest(request) {
  if (request.method === 'OPTIONS') {
    return handleOptions(request);
  }

  const response = await fetch(request);
  const newResponse = new Response(response.body, response);
  const origin = request.headers.get('Origin');

  if (ALLOWED_ORIGINS.includes(origin)) {
    newResponse.headers.set('Access-Control-Allow-Origin', origin);
    newResponse.headers.set('Access-Control-Allow-Credentials', 'true');
  }

  return newResponse;
}

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

Fix for Spring Boot

// Global CORS configuration
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://app.example.com", "https://staging.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
            .allowedHeaders("Authorization", "Content-Type", "X-Requested-With")
            .exposedHeaders("Content-Length", "X-Request-Id")
            .allowCredentials(true)
            .maxAge(86400);
    }
}

// Or per-controller annotation
@RestController
@CrossOrigin(origins = "https://app.example.com", allowCredentials = "true")
public class ApiController {
    @GetMapping("/api/data")
    public ResponseEntity<Map> getData() {
        return ResponseEntity.ok(Map.of("data", "value"));
    }
}

Debugging CORS with curl

# Simulate a preflight request
curl -v -X OPTIONS \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization" \
  https://api.example.com/endpoint 2>&1

# What to check in the response:
# 1. Status code should be 200 or 204
# 2. Access-Control-Allow-Origin must match your origin (or be *)
# 3. Access-Control-Allow-Methods must include your method
# 4. Access-Control-Allow-Headers must include your custom headers

# Simulate a simple GET request with Origin header
curl -v -H "Origin: https://app.example.com" \
  https://api.example.com/endpoint 2>&1 | grep -i 'access-control'

# Expected output:
# < Access-Control-Allow-Origin: https://app.example.com
# < Access-Control-Allow-Credentials: true

Common Gotchas

Wildcard * with Credentials

// THIS WILL NOT WORK:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

// Browser error:
// "The value of the 'Access-Control-Allow-Origin' header in the response
// must not be the wildcard '*' when the request's credentials mode is 'include'."

// FIX: Echo back the specific origin instead of *
// Read the Origin header from the request and validate it against an allowlist

Duplicate Headers

# If both your application AND your reverse proxy set CORS headers,
# you get duplicates. Browsers reject duplicate Access-Control-Allow-Origin.

# Check for duplicates:
curl -sI -H "Origin: https://app.example.com" https://api.example.com/ | grep -i access-control

# If you see:
# Access-Control-Allow-Origin: https://app.example.com
# Access-Control-Allow-Origin: *
# That's the problem. Set CORS in ONE place only.

Redirects Kill CORS

# If your API redirects (301/302), the browser drops the Origin header
# on the redirected request. The redirected server sees no Origin,
# returns no CORS headers, and the browser blocks the response.

# Common cause: HTTP to HTTPS redirect
# http://api.example.com/data -> 301 -> https://api.example.com/data

# Fix: Always use the final URL in your frontend code
fetch('https://api.example.com/data')  // Not http://

Missing Vary: Origin Header

# If you dynamically set Access-Control-Allow-Origin based on the request,
# you MUST include Vary: Origin in the response.
# Without it, CDNs and browser caches may serve a response with the wrong origin.

add_header 'Vary' 'Origin' always;

OPTIONS Returns 405

# Some frameworks/servers reject OPTIONS requests by default.
# If preflight gets a 405 Method Not Allowed, CORS fails.

# In Express, make sure your router handles OPTIONS:
app.options('*', cors());  // Handle preflight for all routes

# In Django, OPTIONS is handled automatically by django-cors-headers
# In Spring Boot, OPTIONS is handled by the CORS filter

Generate Secure API Tokens

Building an API with CORS? Generate cryptographically strong API keys and share them securely through encrypted, self-destructing links.

Generate API Keys

Security Considerations

  • Never use Access-Control-Allow-Origin: * on authenticated endpoints. A wildcard combined with credentials is rejected by browsers, but even without credentials, it allows any website to read your API responses.
  • Validate the Origin header server-side. Do not blindly reflect $http_origin back as the allowed origin. Use an allowlist. Reflecting any origin is equivalent to using *.
  • Restrict Access-Control-Allow-Methods to only the methods your API uses. Do not include DELETE if your API does not have delete endpoints.
  • Set Access-Control-Max-Age to reduce preflight requests. 86400 (24 hours) is a reasonable value. Chrome caps this at 7200 (2 hours) regardless of what you set.
  • Use Access-Control-Expose-Headers carefully. By default, browsers only expose "simple" response headers (Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma). Custom headers like X-Request-Id must be explicitly exposed.

Summary

CORS errors happen because the browser enforces the Same-Origin Policy. The fix is always on the server side: return the correct Access-Control-Allow-Origin header matching the requesting origin. Handle preflight OPTIONS requests with a 204 response and the correct allow headers. Never use wildcard * with credentials. Set always on Nginx add_header directives so CORS headers appear on error responses too. Debug with curl -v including the Origin header. Set CORS in one layer only (either the reverse proxy or the application, not both) to avoid duplicate headers. Add Vary: Origin if you dynamically select the allowed origin.

Related Articles

Continue reading: Fix Nginx 502 Bad Gateway with PHP-FPM, Fix Cookie Without HttpOnly Flag, Fix Incomplete SSL Certificate Chain.

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.