CORS: Understand and Solve It Once and for All

THEJORD Team1 min read
corssecurityapiweb

What is CORS, why it causes errors, how to solve it. Definitive guide to cross-origin errors.

CORS: Understand and Solve It Once and for All

Understanding CORS

CORS (Cross-Origin Resource Sharing) is a security mechanism that restricts web pages from making requests to a different domain than the one serving the page. While it protects users, it often frustrates developers with cryptic error messages. This guide explains how CORS works and provides solutions for common scenarios.

Why CORS Exists

The Same-Origin Policy

Browsers enforce the Same-Origin Policy to prevent malicious sites from accessing data on other sites where a user might be logged in:

// Same origin (allowed)
https://example.com → https://example.com/api  ✓

// Different origin (blocked by default)
https://example.com → https://api.example.com  ✗
https://example.com → http://example.com       ✗
https://example.com → https://example.com:8080 ✗

What Makes an Origin

An origin consists of three parts:

  • Protocol: http or https
  • Domain: example.com, api.example.com
  • Port: 80, 443, 3000, etc.

If any of these differ, it's a different origin.

Common CORS Errors

Typical Error Message

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

Preflight Request Failed

Access to fetch at 'https://api.example.com/data' from origin
'https://example.com' has been blocked by CORS policy: Response
to preflight request doesn't pass access control check.

How CORS Works

Simple Requests

Requests that don't trigger a preflight:

  • Methods: GET, HEAD, POST
  • Standard headers only (Accept, Content-Type with simple values)
  • Content-Type: text/plain, multipart/form-data, or application/x-www-form-urlencoded
// Simple request flow
Client                          Server
  |                                |
  |-- GET /api/data -------------->|
  |                                |
  |<-- Response -------------------|
  |    Access-Control-Allow-Origin: *

Preflight Requests

Complex requests trigger an OPTIONS preflight:

// Preflight request flow
Client                          Server
  |                                |
  |-- OPTIONS /api/data ---------->|
  |   Access-Control-Request-Method: PUT
  |   Access-Control-Request-Headers: Content-Type
  |                                |
  |<-- 204 No Content -------------|
  |    Access-Control-Allow-Origin: *
  |    Access-Control-Allow-Methods: PUT
  |    Access-Control-Allow-Headers: Content-Type
  |                                |
  |-- PUT /api/data -------------->|
  |   Content-Type: application/json
  |                                |
  |<-- Response -------------------|

Server-Side Solutions

Express.js

import cors from 'cors';

// Allow all origins (development only!)
app.use(cors());

// Production configuration
app.use(cors({
  origin: ['https://example.com', 'https://www.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,  // Allow cookies
  maxAge: 86400       // Cache preflight for 24 hours
}));

// Dynamic origin
app.use(cors({
  origin: (origin, callback) => {
    const allowedOrigins = ['https://example.com'];
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}));

Manual Headers

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://example.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true');

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

  next();
});

Next.js API Routes

// pages/api/data.js or app/api/data/route.js
export async function GET(request) {
  const response = NextResponse.json({ data: 'hello' });

  response.headers.set('Access-Control-Allow-Origin', '*');
  response.headers.set('Access-Control-Allow-Methods', 'GET, POST');

  return response;
}

// For OPTIONS preflight
export async function OPTIONS() {
  return new Response(null, {
    status: 204,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  });
}

Nginx

location /api/ {
    add_header 'Access-Control-Allow-Origin' 'https://example.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') {
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }

    proxy_pass http://backend;
}

Client-Side Considerations

Credentials (Cookies)

// Include cookies in requests
fetch('https://api.example.com/data', {
  credentials: 'include'  // Required for cookies
});

// Server must respond with:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.com  // Cannot be *

Custom Headers

// Custom headers trigger preflight
fetch('https://api.example.com/data', {
  headers: {
    'Authorization': 'Bearer token',
    'X-Custom-Header': 'value'
  }
});

// Server must allow these headers:
Access-Control-Allow-Headers: Authorization, X-Custom-Header

Development Workarounds

Proxy in Development

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

// Now requests to /api go through proxy (no CORS)
fetch('/api/data');

Next.js Rewrites

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'https://api.example.com/:path*'
      }
    ];
  }
};

Common Mistakes

Using Wildcard with Credentials

// This won't work!
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

// Fix: specify exact origin
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true

Missing Preflight Handler

// OPTIONS requests return 404
// Fix: handle OPTIONS method
app.options('/api/*', cors());  // Preflight for all routes

Forgetting Allowed Headers

// Error: Request header field authorization is not allowed
// Fix: add to allowed headers
Access-Control-Allow-Headers: Content-Type, Authorization

Debugging CORS

Browser DevTools

  1. Open Network tab
  2. Find the failed request
  3. Check Response Headers for CORS headers
  4. Look for preflight OPTIONS request

Useful Headers to Check

// Request headers
Origin: https://example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type

// Response headers (should be present)
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: Content-Type

Tools and Resources

For debugging and development:

Conclusion

CORS errors are frustrating but solvable. Key takeaways:

  • CORS is a browser security feature, not a server bug
  • Configure CORS on the server, not the client
  • Be specific about allowed origins in production
  • Use proxies during development to avoid CORS entirely
  • Check for preflight (OPTIONS) handling

For more developer resources, explore our free online tools. For the full specification, see MDN CORS documentation.