DEVESSENTIALS

CORS Explained: What It Is and How to Fix It

You've seen the error: "Access to fetch at 'https://api.example.com' from origin 'https://yourapp.com' has been blocked by CORS policy." This guide explains exactly why the browser does this, how CORS headers work, and how to fix every scenario you'll encounter.

What Is CORS?

CORS stands for Cross-Origin Resource Sharing. It is a browser security mechanism that restricts how JavaScript running on one origin (domain + port + protocol) can request resources from a different origin.

The key word is browser. CORS is enforced by the browser, not the server. The server sends headers that tell the browser whether to allow or block the response. If you make the same request with curl or Postman, CORS doesn't apply — there's no browser enforcing it.

Why Does CORS Exist?

Without CORS, the following attack would be trivial:

  1. You visit evil.com.
  2. JavaScript on that page silently calls fetch('https://yourbank.com/api/transfer', { method: 'POST', credentials: 'include' }).
  3. Your browser sends your banking cookies with the request.
  4. The bank processes the transfer.

This is called a Cross-Site Request Forgery (CSRF) attack. CORS is one of the browser's defenses against it. The browser blocks responses from origins that haven't explicitly opted in to cross-origin access.

The Same-Origin Policy

The browser's default rule is the Same-Origin Policy: JavaScript can only read responses from the same origin. Two URLs have the same origin if and only if they share the same protocol, host, and port:

https://app.example.com/api   ← same origin
https://app.example.com/data  ← same origin

http://app.example.com/api    ← different origin (http vs https)
https://api.example.com/data  ← different origin (different subdomain)
https://app.example.com:3000  ← different origin (different port)

CORS is the mechanism that allows servers to relax the same-origin policy for specific origins.

How CORS Headers Work

Simple Requests

For simple requests (GET, POST with certain content types, no custom headers), the browser adds an Origin header to the request:

GET /api/data HTTP/1.1
Host: api.example.com
Origin: https://yourapp.com

The server responds with Access-Control-Allow-Origin:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://yourapp.com
Content-Type: application/json

The browser checks: does the value of Access-Control-Allow-Origin match the request's origin? If yes, it allows the JavaScript to read the response. If not, it blocks it.

Preflight Requests

For requests that could modify data (PUT, DELETE, PATCH, POST with JSON body, or any request with custom headers like Authorization), the browser first sends a preflight OPTIONS request:

OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://yourapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

The server must respond to the preflight with the appropriate permissions:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://yourapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Only if the preflight succeeds will the browser send the actual request. Access-Control-Max-Age caches the preflight result so the browser doesn't repeat it on every request.

Fixing CORS Errors

Fix 1: Configure the Server (the correct fix)

The proper solution is always to configure the server to return the right headers. Here's how to do it in the most common backends:

Node.js / Express:

const cors = require('cors');

app.use(cors({
  origin: 'https://yourapp.com',       // specific origin
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,                    // if using cookies
}));

Python / FastAPI:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://yourapp.com"],
    allow_methods=["*"],
    allow_headers=["*"],
    allow_credentials=True,
)

Nginx:

location /api/ {
    add_header Access-Control-Allow-Origin "https://yourapp.com";
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
    add_header Access-Control-Allow-Headers "Authorization, Content-Type";

    if ($request_method = OPTIONS) {
        return 204;
    }
}

Fix 2: Proxy Through Your Own Backend (for third-party APIs)

If you're calling a third-party API that doesn't support CORS (and you can't change their server), proxy the request through your own backend:

// Instead of: fetch('https://third-party-api.com/data')
// Call your own backend:
fetch('/api/proxy/data')

// Your backend then calls the third-party API server-to-server
// (no browser = no CORS enforcement)

Fix 3: Use a Reverse Proxy in Development

During local development, configure your dev server to proxy API requests so they appear to come from the same origin:

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3001',
        changeOrigin: true,
      },
    },
  },
}

Common Mistakes

  • Setting Access-Control-Allow-Origin: * with credentials. The spec forbids this. Use a specific origin when credentials are involved.
  • Only handling GET but not OPTIONS. Preflight requests use OPTIONS. If your server returns 404 or 405 for OPTIONS, every non-simple request will fail.
  • Setting CORS headers in JavaScript on the client. This does nothing. CORS headers must come from the server response — the browser ignores any client-side attempt to set them.
  • Caching old preflight responses. If you change your CORS config, the browser may use a cached preflight result. Clear browser cache or reduce Access-Control-Max-Age during development.

Inspecting CORS with DevTools

Open Chrome DevTools → Network tab → click the failed request → look at:

  • Request Headers: confirm the Origin header is present
  • Response Headers: check for Access-Control-Allow-Origin
  • Console: the error message tells you exactly which header is missing or mismatched

If you see a preflight OPTIONS request in the Network tab with a non-2xx response, that's why the actual request never fires.


Testing a CORS-affected API response? Use the JSON Formatter to inspect the response body, or JWT Decoder to check the Authorization token you're sending.

Frequently Asked Questions

Does CORS affect server-to-server requests?

No. CORS is enforced entirely by the browser. When your Node.js, Python, or Go server makes an HTTP request to another server, there is no browser involved and no CORS check. CORS only applies to JavaScript running inside a browser (or browser-based tools like Fetch/XHR from a web page). This is why API calls work fine in Postman or curl but fail in the browser.

Why can't I just disable CORS in the browser?

CORS exists to protect users, not developers. Without it, any website the user visits could silently make authenticated requests to their banking app, email, or internal corporate tools — reading private data and taking actions on their behalf. You can't disable CORS without disabling the security it provides. The correct fix is always to configure the server to send the right headers.

What is a CORS preflight request?

For requests that could have side effects (POST, PUT, DELETE, or any request with custom headers), the browser first sends an OPTIONS request to check if the server allows the cross-origin operation. This is the preflight. If the server responds with the right Access-Control-Allow-* headers, the browser proceeds with the actual request. If not, the browser blocks the request without ever sending it.

Is using a CORS proxy a good solution?

In production, no. A CORS proxy routes your request through a server that adds the Access-Control-Allow-Origin header. This works but it means all your request data passes through a third-party server — a serious security risk for authenticated APIs. CORS proxies are acceptable for public APIs with no sensitive data during development or prototyping, but the correct fix is always configuring your own backend.

Can I use Access-Control-Allow-Origin: * with credentials?

No — this combination is explicitly forbidden by the CORS spec. If you set credentials: 'include' in your fetch request (to send cookies or HTTP auth), the server must respond with a specific origin in Access-Control-Allow-Origin (not *) and also include Access-Control-Allow-Credentials: true. Using a wildcard origin with credentials would allow any website to make authenticated requests on behalf of the user, which is exactly what CORS was designed to prevent.