Fixing CORS Errors: Understanding Cross-Origin Resource Sharing

Two floating server islands labeled Origin A and Origin B connected by a bridge with a wrench icon, showing blocked and allowed cross-origin requests, illustrating how CORS controls communication between different web origins

If you've ever opened your browser's console and seen something like "Access to fetch at 'https://api.example.com' from origin 'https://yoursite.com' has been blocked by CORS policy," you've hit a CORS error. Knowing how to fix CORS errors starts with understanding why browsers block these requests in the first place, and then knowing exactly which server-side headers and configurations tell the browser it's safe to proceed.

The Browser Same-Origin Policy

The browser same-origin policy is a security rule built into every modern browser. It says: JavaScript running on one origin cannot read the response from a request made to a different origin, unless that other origin explicitly permits it.

An "origin" is the combination of three things:

  • Protocol (http vs. https)
  • Domain (example.com vs. api.example.com)
  • Port (3000 vs. 8080)

If any one of those three differs between your page's URL and the URL you're fetching from, you're making a cross-origin request, and the same-origin policy kicks in. So https://app.example.com and https://api.example.com are different origins, even though they share the same root domain.

The same-origin policy only restricts what JavaScript can read from a response. The browser still sends the request. The block happens when the browser receives the response and checks whether it's allowed to expose it to your script.

What Is CORS and Why Does It Exist

CORS stands for Cross-Origin Resource Sharing . It's a mechanism, defined by the WHATWG Fetch specification , that lets servers opt in to allowing cross-origin requests from specific origins. Without CORS, the same-origin policy would block every cross-origin API call, which would make modern web apps essentially impossible to build.

CORS works entirely through HTTP headers. The server includes special response headers that tell the browser: "Yes, I trust this origin. You can let the script read this response." The browser enforces those permissions. Your JavaScript code never sees them directly.

What Triggers a CORS Error

A CORS error appears in your console when the browser sends a cross-origin request and the server either doesn't send the right headers or sends headers that don't match your origin. Here are the most common triggers:

  • Your frontend is on http://localhost:3000 and your API is on http://localhost:8000 (different ports = different origins).
  • You're calling a third-party API that hasn't enabled CORS for your domain.
  • Your server returns an error (like a 500) before the CORS headers are added.
  • You're sending a request with a custom header (like Authorization ) and the server doesn't explicitly allow it.
  • Your server returns Access-Control-Allow-Origin: * but the request includes credentials (cookies or HTTP auth), which requires a specific origin, not a wildcard.
CORS errors are always a server-side problem . You cannot fix them by changing your JavaScript. The fix lives in the server's response headers or proxy configuration.

How CORS Preflight Requests Work

Not every cross-origin request triggers a preflight. Browsers split cross-origin requests into two categories: "simple" requests and requests that need a preflight check.

Simple requests go straight through if they meet all of these conditions:

  • Method is GET, POST, or HEAD.
  • Headers are limited to Accept , Accept-Language , Content-Language , and Content-Type (with values of application/x-www-form-urlencoded , multipart/form-data , or text/plain ).
  • No custom headers like Authorization .

Everything else triggers a CORS preflight request . This is an automatic OPTIONS request the browser sends before your actual request. It asks the server: "I'm about to send a DELETE request from origin https://app.example.com with an Authorization header. Is that okay?"

A preflight request looks like this:

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

The server must respond with the appropriate CORS headers and a 200 or 204 status. If it does, the browser sends the actual request. If it doesn't, the browser blocks the request and you see a CORS error in the console, even though your actual request was never sent.

You can see preflight requests in the Network tab of DevTools. Look for OPTIONS requests to your API endpoint. If they're returning 404 or 405, your server isn't handling the OPTIONS method at that route.

The CORS Headers That Actually Fix the Problem

These are the HTTP response headers your server needs to send. Each one controls a specific aspect of what the browser allows.

Header What It Controls Example Value
Access-Control-Allow-Origin Which origins can read the response https://app.example.com or *
Access-Control-Allow-Methods Which HTTP methods are allowed GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers Which request headers are allowed Authorization, Content-Type
Access-Control-Allow-Credentials Whether cookies/auth can be included true
Access-Control-Max-Age How long the browser caches preflight results (seconds) 86400
Access-Control-Expose-Headers Which response headers JavaScript can access X-Custom-Header, X-Request-Id

The most important one is Access-Control-Allow-Origin . Without it, the browser blocks every cross-origin read. The value must exactly match the requesting origin, or be * for public APIs that don't use credentials.

You cannot use Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true . The browser will reject this combination. When you need credentials, you must specify the exact origin.

CORS Configuration on Common Backends

Express (Node.js)

The easiest way is the cors npm package. Install it with npm install cors , then use it as middleware:

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

// Allow a specific origin
const corsOptions = {
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Authorization', 'Content-Type'],
  credentials: true,
  maxAge: 86400
};

app.use(cors(corsOptions));

// Handle preflight for all routes
app.options('*', cors(corsOptions));

app.listen(3000);

If you need to allow multiple origins dynamically, pass a function to the origin option:

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

const corsOptions = {
  origin: function (origin, callback) {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true
};

app.use(cors(corsOptions));

Nginx

Add CORS headers directly in your Nginx server block. This approach handles preflight requests too:

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

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

        # Add CORS headers to all other responses
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
        add_header 'Access-Control-Allow-Credentials' 'true';

        proxy_pass http://localhost:8080;
    }
}
Nginx's add_header directive only applies to 2xx and 3xx responses by default. If your backend returns a 4xx or 5xx, the CORS headers won't be added. Use add_header ... always; at the end of each directive to fix this.

Apache

Enable mod_headers first ( sudo a2enmod headers on Debian/Ubuntu), then add this to your .htaccess or virtual host config:

Header always set Access-Control-Allow-Origin "https://app.example.com"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
Header always set Access-Control-Allow-Credentials "true"

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

Using a Proxy During Development

If you're using Create React App or Vite and just need to bypass CORS locally, you can proxy API requests through your dev server instead of dealing with CORS at all. In Create React App, add this to package.json :

"proxy": "http://localhost:8000"

In Vite's vite.config.js :

export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true
      }
    }
  }
}

This only works in development. In production, you still need proper CORS headers or a reverse proxy. This is also related to how API rate limiting interacts with cross-origin requests, since rate limits are usually enforced at the server or proxy layer alongside CORS.

Common CORS Mistakes to Avoid

  • Setting CORS headers in the frontend. CORS headers go on the server response. Setting them in your JavaScript fetch call does nothing.
  • Forgetting to handle OPTIONS. If your server returns 404 or 405 for OPTIONS requests, preflight fails and your actual request never goes through.
  • Using a wildcard with credentials. Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true together always fail. Use a specific origin.
  • Missing CORS headers on error responses. If your server throws a 500 before the CORS middleware runs, the browser sees a response with no CORS headers and blocks it. Make sure CORS middleware runs before your route handlers and error handlers.
  • Trailing slashes in the origin. https://app.example.com/ is not the same as https://app.example.com . Browsers send the origin without a trailing slash.
  • Not reflecting the request origin dynamically. If you hard-code a single origin but need to support multiple, requests from unlisted origins will fail silently.

If you're building or debugging APIs and want to understand how the request-response cycle works at a deeper level, it's also worth reading about scheduled server-side tasks , since many API backends use scheduled jobs that also need proper origin handling when making outbound requests.

Understanding how your JSON payloads are structured can also save debugging time. A well-formatted response body is much easier to inspect when tracing CORS issues. Our JSON beautifier can help you quickly read and verify API response bodies when you're stepping through network requests in DevTools.

JSON beautifier tool for inspecting API responses when debugging CORS errors

Inspect API responses faster while debugging CORS errors

When you're tracing cross-origin requests in the browser's Network tab, raw JSON responses can be hard to read. Use our free JSON beautifier to instantly format and inspect API response bodies so you can spot missing CORS headers or malformed payloads in seconds.

Try the JSON Beautifier →

CORS is enforced entirely by the browser, not by the server or the HTTP protocol itself. Tools like Postman and curl don't implement the same-origin policy, so they send and receive cross-origin requests without any restrictions. If your request works in Postman but fails in the browser, the problem is definitely missing or incorrect CORS headers on your server's response.

Browser extensions that disable CORS (like "Allow CORS: Access-Control-Allow-Origin" for Chrome) can unblock requests in your own browser during development, but they are not a real fix. They only affect your browser, not your users' browsers. Any production fix must come from the server. Extensions are useful for quick local testing, but never ship code that depends on them.

A simple request goes directly to the server without a prior check. It must use GET, POST, or HEAD, and cannot include custom headers or non-standard Content-Type values. A preflight request is an automatic OPTIONS request the browser sends first when your request doesn't meet those conditions. The server must respond to the OPTIONS request with the correct CORS headers before the browser will send your actual request.

It depends on what your API does. For truly public APIs that serve read-only, non-sensitive data, a wildcard is fine. For any API that uses authentication, user data, or cookies, you should specify exact allowed origins instead. A wildcard combined with credentials is also blocked by browsers by design, so it won't work anyway when credentials are involved.

Several things can cause this. Your server might be returning an error (4xx or 5xx) before the CORS middleware runs, so the headers never get added. You might have a trailing slash mismatch in the origin value. Your OPTIONS preflight might be returning a 404. Or you're using a wildcard with credentials. Check the Network tab in DevTools, look at the actual response headers on both the OPTIONS and the main request, and trace exactly where the headers are missing.

The Access-Control-Allow-Origin header can only contain one value at a time, so you can't list multiple origins directly. Instead, read the incoming Origin request header on the server, check it against your allowlist, and if it matches, reflect that exact origin back in the response header. The Express example in this article shows exactly how to do this with a dynamic origin function in the cors middleware.