Introduction

CDN CORS (Cross-Origin Resource Sharing) policy errors occur when web applications running on one domain attempt to load resources (fonts, APIs, images, scripts) from a CDN on a different domain, and the CDN fails to return proper CORS headers. Browsers enforce same-origin policy by default, blocking cross-origin requests unless the server explicitly allows them via CORS headers. Common causes include missing Access-Control-Allow-Origin header, incorrect origin specified in CORS header, wildcard (*) used when credentials required, missing Access-Control-Allow-Methods for preflight requests, missing Access-Control-Allow-Headers for custom headers, preflight OPTIONS request not handled correctly, CORS headers not cached causing repeated preflight, CDN origin not passing CORS headers through, and Vary header missing causing caching issues. The fix requires understanding CORS protocol, preflight request handling, proper header configuration, and CDN-specific CORS settings. This guide provides production-proven troubleshooting for CORS issues across Cloudflare, AWS CloudFront, Fastly, Akamai, and generic CDN implementations.

Symptoms

  • Browser console: Access to fetch at 'https://cdn.example.com/file.js' from origin 'https://app.example.com' has been blocked by CORS policy
  • No 'Access-Control-Allow-Origin' header is present on the requested resource
  • The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include'
  • Preflight request fails: Response to preflight request doesn't pass access control check
  • The value of the 'Access-Control-Allow-Credentials' header is 'true' but the value of the 'Access-Control-Allow-Origin' header is '*'
  • Fonts not loading: Access to font at...blocked by CORS policy
  • XMLHttpRequest or fetch fails with network error
  • CORS error only in production, not development
  • Some CORS requests work, others fail intermittently

Common Causes

  • CDN not configured to add CORS headers
  • Origin server CORS headers not cached by CDN
  • Wildcard (*) used with credentials (not allowed by CORS spec)
  • Preflight OPTIONS request returns 404 or 405
  • Access-Control-Allow-Methods missing required method
  • Access-Control-Allow-Headers missing custom headers
  • Vary: Origin header missing causing wrong cached response
  • CORS headers only returned for some HTTP methods
  • Dynamic origin validation failing
  • CDN edge caching non-CORS response and serving to all origins

Step-by-Step Fix

### 1. Diagnose CORS issues

Check CORS headers:

```bash # Check response headers curl -I -H "Origin: https://app.example.com" \ https://cdn.example.com/file.js

# Look for: # Access-Control-Allow-Origin: https://app.example.com # Access-Control-Allow-Credentials: true # Access-Control-Allow-Methods: GET, POST, OPTIONS # Access-Control-Allow-Headers: Content-Type, Authorization # Vary: Origin

# Check preflight request curl -X OPTIONS -I \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: GET" \ -H "Access-Control-Request-Headers: Content-Type" \ https://cdn.example.com/file.js

# Should return 200 or 204 with CORS headers

# Test from browser console fetch('https://cdn.example.com/file.js', { mode: 'cors', credentials: 'include' }) .then(r => r.text()) .then(console.log) .catch(console.error) ```

Browser DevTools CORS debugging:

```javascript // Chrome DevTools > Network tab // 1. Check request has "Origin" header // 2. Check response has "Access-Control-Allow-Origin" // 3. Check for preflight OPTIONS request // 4. Check preflight response headers

// Chrome DevTools > Console // CORS errors show: // - Failed origin // - Missing header // - Invalid credential combination

// Chrome CORS error codes: // CORB: Cross-Origin Read Blocking // CORP: Cross-Origin Resource Policy // COEP: Cross-Origin Embedder Policy ```

### 2. Configure Cloudflare CORS

Cloudflare Workers for CORS:

```javascript // Cloudflare Worker to add CORS headers // Workers > Create Worker > Edit

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

async function handleRequest(request) { // Handle preflight OPTIONS requests if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: { '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', // Cache preflight for 24 hours 'Access-Control-Allow-Credentials': 'true' } }) }

// Fetch from origin const response = await fetch(request)

// Add CORS headers to response const corsHeaders = { 'Access-Control-Allow-Origin': 'https://app.example.com', 'Access-Control-Allow-Credentials': 'true', 'Vary': 'Origin' // Important for caching }

return new Response(response.body, { status: response.status, headers: { ...Object.fromEntries(response.headers), ...corsHeaders } }) }

// For multiple allowed origins (not wildcard with credentials) const ALLOWED_ORIGINS = [ 'https://app.example.com', 'https://admin.example.com', 'https://shop.example.com' ]

function getCorsHeaders(origin) { if (ALLOWED_ORIGINS.includes(origin)) { return { 'Access-Control-Allow-Origin': origin, 'Access-Control-Allow-Credentials': 'true', 'Vary': 'Origin' } } return {} } ```

Cloudflare Page Rules (simpler approach):

``` # Page Rules > Create Page Rule # URL: cdn.example.com/*

# Settings: # Custom Headers: # Access-Control-Allow-Origin: https://app.example.com # Access-Control-Allow-Methods: GET, OPTIONS # Access-Control-Allow-Headers: Content-Type # Vary: Origin ```

### 3. Configure CloudFront CORS

CloudFront origin response:

```python # Lambda@Edge for CORS headers # Requires: Lambda function deployed in us-east-1

def lambda_handler(event, context): response = event['Records'][0]['cf']['response'] headers = response['headers']

# Add CORS headers headers['access-control-allow-origin'] = [{ 'key': 'Access-Control-Allow-Origin', 'value': 'https://app.example.com' }]

headers['access-control-allow-credentials'] = [{ 'key': 'Access-Control-Allow-Credentials', 'value': 'true' }]

headers['vary'] = [{ 'key': 'Vary', 'value': 'Origin' }]

return response

# For preflight handling (origin request trigger) def handle_preflight(event, context): request = event['Records'][0]['cf']['request']

if request['method'] == 'OPTIONS': return { 'status': '204', 'statusDescription': 'No Content', 'headers': { 'access-control-allow-origin': [{ 'key': 'Access-Control-Allow-Origin', 'value': 'https://app.example.com' }], 'access-control-allow-methods': [{ 'key': 'Access-Control-Allow-Methods', 'value': 'GET, POST, PUT, DELETE, OPTIONS' }], 'access-control-allow-headers': [{ 'key': 'Access-Control-Allow-Headers', 'value': 'Content-Type, Authorization' }], 'access-control-max-age': [{ 'key': 'Access-Control-Max-Age', 'value': '86400' }], 'access-control-allow-credentials': [{ 'key': 'Access-Control-Allow-Credentials', 'value': 'true' }] } }

return request ```

CloudFront Functions (lighter weight):

```javascript // CloudFront Functions (viewer request/response) function handler(event) { var request = event.request; var origin = request.headers.origin;

// Handle preflight if (request.method === 'OPTIONS') { return { statusCode: 204, headers: { 'access-control-allow-origin': { value: 'https://app.example.com' }, 'access-control-allow-methods': { value: 'GET, OPTIONS' }, 'access-control-allow-headers': { value: 'Content-Type' }, 'access-control-max-age': { value: '86400' }, 'vary': { value: 'Origin' } } }; }

return request; } ```

### 4. Configure origin server CORS

Nginx origin CORS configuration:

```nginx server { listen 443 ssl; server_name cdn.example.com;

# CORS headers add_header Access-Control-Allow-Origin "https://app.example.com" always; add_header Access-Control-Allow-Credentials "true" always; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always; add_header Access-Control-Allow-Headers "Content-Type, Authorization" always; add_header Access-Control-Max-Age "86400" always; add_header Vary "Origin" always;

# Handle preflight location / { if ($request_method = OPTIONS) { add_header Access-Control-Allow-Origin "https://app.example.com"; add_header Access-Control-Allow-Credentials "true"; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type, Authorization"; add_header Access-Control-Max-Age "86400"; add_header Content-Type "text/plain charset=UTF-8"; add_header Content-Length 0; return 204; }

# Normal request processing try_files $uri $uri/ =404; }

# For multiple origins (dynamic) # Note: Multiple origin values not supported, use map map $http_origin $cors_origin { default ""; "https://app.example.com" "https://app.example.com"; "https://admin.example.com" "https://admin.example.com"; "https://shop.example.com" "https://shop.example.com"; }

add_header Access-Control-Allow-Origin $cors_origin always; } ```

Apache origin CORS configuration:

```apache # .htaccess or httpd.conf

# Enable CORS for specific origin <IfModule mod_headers.c> Header set Access-Control-Allow-Origin "https://app.example.com" Header set Access-Control-Allow-Credentials "true" Header set Access-Control-Allow-Methods "GET, POST, OPTIONS" Header set Access-Control-Allow-Headers "Content-Type, Authorization" Header set Access-Control-Max-Age "86400" Header append Vary "Origin" </IfModule>

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

# For multiple origins with SetEnvIf <IfModule mod_headers.c> SetEnvIf Origin "^https://app\.example\.com$" ORIGIN=$0 SetEnvIf Origin "^https://admin\.example\.com$" ORIGIN=$0 SetEnvIf Origin "^https://shop\.example\.com$" ORIGIN=$0

Header set Access-Control-Allow-Origin %{ORIGIN}e env=ORIGIN Header set Access-Control-Allow-Credentials "true" Header set Vary "Origin" </IfModule> ```

### 5. Fix CORS with credentials

Credentials mode requirements:

```javascript // WRONG: Wildcard with credentials (not allowed) // Response header: // Access-Control-Allow-Origin: * // INVALID with credentials! // Access-Control-Allow-Credentials: true

// Browser error: // The value of the 'Access-Control-Allow-Origin' header is '*' // but the request's credentials mode is 'include'

// CORRECT: Specific origin with credentials // Response header: Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Credentials: true

// Client-side with credentials fetch('https://cdn.example.com/api/data', { method: 'GET', credentials: 'include', // Send cookies headers: { 'Content-Type': 'application/json' } }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error)); ```

CORS credential modes:

```javascript // credentials: 'omit' (default) // - No cookies sent // - CDN can use Access-Control-Allow-Origin: *

// credentials: 'same-origin' // - Only send cookies for same-origin requests // - Cross-origin requests don't send cookies

// credentials: 'include' // - Always send cookies // - Requires specific origin (no wildcard) // - Requires Access-Control-Allow-Credentials: true

// Example for each fetch('/api/data', { credentials: 'omit' }); fetch('/api/data', { credentials: 'same-origin' }); fetch('https://cdn.example.com/api/data', { credentials: 'include' }); ```

### 6. Fix preflight request handling

Preflight request requirements:

```bash # Preflight is triggered when: # - Method is not GET, HEAD, or POST # - POST has non-simple Content-Type (not application/x-www-form-urlencoded, multipart/form-data, or text/plain) # - Custom headers are set

# Example that triggers preflight: fetch('https://cdn.example.com/api/data', { method: 'PUT', // Non-simple method headers: { 'Content-Type': 'application/json', // Non-simple Content-Type 'X-Custom-Header': 'value' // Custom header }, body: JSON.stringify({ data: 'value' }) });

# Server must respond to OPTIONS: # OPTIONS /api/data HTTP/1.1 # Host: cdn.example.com # Origin: https://app.example.com # Access-Control-Request-Method: PUT # Access-Control-Request-Headers: Content-Type, X-Custom-Header

# 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, X-Custom-Header # Access-Control-Max-Age: 86400 ```

Cache preflight responses:

``` # Access-Control-Max-Age: seconds to cache preflight # Reduces preflight requests for same resource

# Cloudflare/CloudFront/Akamai: Access-Control-Max-Age: 86400 # 24 hours

# Browser will cache for up to 24 hours # After that, preflight required again

# Note: Some browsers have their own max limits # Chrome: up to 24 hours # Firefox: up to 24 hours # Safari: up to 24 hours ```

### 7. Fix Vary header for caching

Vary header importance:

``` # Without Vary: Origin, CDN caches first response # Request 1: Origin A -> Response with CORS for A (cached) # Request 2: Origin B -> Same cached response (wrong!) # Result: Origin B gets CORS error

# With Vary: Origin, CDN caches per-origin # Request 1: Origin A -> Response with CORS for A (cached for A) # Request 2: Origin B -> Response with CORS for B (cached for B) # Result: Both origins work correctly

# Always include Vary: Origin with dynamic CORS Vary: Origin ```

CDN cache key configuration:

``` # CloudFront cache behavior # Cache Based On: # - Whitelist: Origin (add to whitelist)

# Cloudflare # Caching > Configuration > Cache Keys # Include Origin header in cache key

# Or via Page Rule: # Cache Level: Cache Everything # Cache Key: Include all of the above + Origin ```

### 8. Monitor CORS errors

CORS error monitoring:

```javascript // Client-side CORS error logging window.addEventListener('error', function(event) { if (event.message && event.message.includes('CORS')) { // Send to monitoring service fetch('/api/log-cors-error', { method: 'POST', body: JSON.stringify({ message: event.message, url: event.target?.src || window.location.href, timestamp: new Date().toISOString() }) }); } }, true);

// Server-side CORS metrics // Log all CORS preflight requests // Track: origin, method, headers requested ```

Prevention

  • Always include Vary: Origin header when using dynamic CORS
  • Use specific origins instead of wildcard when credentials required
  • Handle OPTIONS preflight requests at CDN and origin
  • Set appropriate Access-Control-Max-Age for preflight caching
  • Test CORS from all expected origins before production
  • Document allowed origins and methods for CDN team
  • Monitor CORS errors in production with alerting
  • Use Cloudflare Workers or Lambda@Edge for consistent CORS across origins
  • **Access to fetch blocked by CORS policy**: Missing or incorrect CORS headers
  • **No Access-Control-Allow-Origin header**: CORS not configured
  • **Wildcard with credentials invalid**: Cannot use * with credentials: include
  • **Preflight request failed**: OPTIONS not handled correctly
  • **Access-Control-Allow-Headers mismatch**: Requested header not in allowed list