DEV Community

Cover image for CORS Errors Explained: Every Fix, Every Framework (2026 Guide)
Rahul J
Rahul J

Posted on • Originally published at alldevtoolshub.com

CORS Errors Explained: Every Fix, Every Framework (2026 Guide)

CORS Errors Explained: Every Fix, Every Framework (2026 Guide)

TL;DR — A CORS error means the browser blocked a cross-origin request because the server did not explicitly allow it. The fix is always server-side: return the correct Access-Control-Allow-Origin header from your backend. This guide covers every CORS error type, a step-by-step diagnosis flow, and copy-paste fixes for Express, FastAPI, Next.js, nginx, Cloudflare Workers, and Vercel. You can inspect and validate your CORS headers live with the CORS Header Checker — no curl, no Postman, no install.


What CORS Actually Is (and Why the Browser Enforces It)

The Same-Origin Policy (SOP) is a browser security rule: JavaScript running on https://myapp.com can only read responses from requests made to the same origin — same scheme, same host, same port. Everything else is cross-origin.

CORS — Cross-Origin Resource Sharing — is the mechanism that lets servers selectively relax the Same-Origin Policy. A server adds HTTP headers to its responses that tell the browser: "it is okay to share this response with code from origin X." Without those headers, the browser reads the response, then silently discards it and throws a CORS error into your console.

Three things to burn into memory before you read further:

  1. CORS is enforced by the browser, not the server. curl and Postman do not check CORS — they always get the response. Only browsers do CORS. If your API works in Postman but fails in the browser, CORS is almost certainly why.
  2. The fix is server-side, always. Browser extensions that "disable CORS" are masking the problem in your local browser only. They break for every real user. Never ship code that depends on them.
  3. Preflight is a separate request. For non-simple requests (anything with a custom header, a JSON body, or methods other than GET/POST), the browser sends an OPTIONS request first to ask for permission. Your server must handle this correctly.

The Four CORS Error Types — Diagnosed from the Console Message

Error 1: "No 'Access-Control-Allow-Origin' header is present"

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

Cause: The server returned a response with no CORS headers at all. This is the most common error. Your API is running and returning data, but it never told the browser it was okay to share that data.

Fix: Add Access-Control-Allow-Origin to your server's response headers. See the framework-specific fixes below.


Error 2: "The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include'"

Access to fetch at 'https://api.example.com' from origin 'https://myapp.com'
has been blocked by CORS policy: 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'.
Enter fullscreen mode Exit fullscreen mode

Cause: You set Access-Control-Allow-Origin: * but your frontend sends credentials (cookies, Authorization headers, or TLS client certificates) using fetch with credentials: 'include'. These two settings are mutually exclusive — the wildcard is forbidden when credentials are involved.

Fix: Change * to the exact origin of your frontend. You must also set Access-Control-Allow-Credentials: true.

// ❌ Wrong — won't work with credentials
res.setHeader('Access-Control-Allow-Origin', '*');

// ✅ Correct
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Credentials', 'true');
Enter fullscreen mode Exit fullscreen mode

Error 3: "Request header field X is not allowed by Access-Control-Allow-Headers in preflight response"

Access to fetch at 'https://api.example.com' has been blocked by CORS policy:
Request header field Authorization is not allowed by 
Access-Control-Allow-Headers in preflight response.
Enter fullscreen mode Exit fullscreen mode

Cause: Your frontend includes a custom header (Authorization, Content-Type: application/json, X-Custom-Header, etc.) that your server does not explicitly allow in its preflight response.

Fix: Add the missing header to Access-Control-Allow-Headers in your preflight OPTIONS response:

Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header
Enter fullscreen mode Exit fullscreen mode

Error 4: "Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response"

Cause: Your frontend uses PUT, DELETE, PATCH, or another non-simple method that your server has not explicitly allowed.

Fix: Add the method to Access-Control-Allow-Methods:

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
Enter fullscreen mode Exit fullscreen mode

Step-by-Step CORS Diagnosis Flow

Before diving into framework fixes, diagnose which layer the problem lives in:

Step 1 — Confirm it is actually a CORS error. Open DevTools → Network tab → find the failed request. Look at the Response headers tab. Is Access-Control-Allow-Origin missing? That is Error 1. Is the value * but you send cookies? That is Error 2.

Step 2 — Check if the server is returning anything at all. If the response status is 0 or the request shows "failed" in red with no status code, the request may be getting blocked before it reaches your server (firewall, missing DNS, TLS error). CORS errors show a status code but the body is blocked.

Step 3 — Inspect the preflight. Look for an OPTIONS request to the same URL in the Network tab. If it is missing when you expect it, your browser decided the request is "simple" and skipped it. If it exists but returns a non-2xx status, your server is rejecting OPTIONS at the routing level before CORS headers are set.

Step 4 — Test with the CORS checker. Paste your API endpoint into the AllDevToolsHub CORS Header Checker to see exactly which headers your server returns, without writing a single line of code.


Fixes by Framework

Express.js (Node.js)

Install the cors package:

npm install cors
Enter fullscreen mode Exit fullscreen mode

Simple — allow one origin:

const cors = require('cors');

app.use(cors({
  origin: 'https://myapp.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true, // only if you send cookies/auth headers
}));
Enter fullscreen mode Exit fullscreen mode

Dynamic — allow multiple origins from a list:

const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error(`CORS blocked: ${origin}`));
    }
  },
  credentials: true,
}));
Enter fullscreen mode Exit fullscreen mode

Handle preflight explicitly (necessary if your route-level middleware conflicts):

app.options('*', cors()); // Enable pre-flight across all routes
Enter fullscreen mode Exit fullscreen mode

FastAPI (Python)

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://myapp.com"],  # use ["*"] only for fully public APIs
    allow_credentials=True,               # set False if using allow_origins=["*"]
    allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
    allow_headers=["Content-Type", "Authorization"],
)
Enter fullscreen mode Exit fullscreen mode

Next.js (App Router)

Add headers in next.config.ts:

// next.config.ts
const nextConfig = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: 'https://myapp.com' },
          { key: 'Access-Control-Allow-Methods', value: 'GET,POST,PUT,DELETE,OPTIONS' },
          { key: 'Access-Control-Allow-Headers', value: 'Content-Type,Authorization' },
          { key: 'Access-Control-Allow-Credentials', value: 'true' },
        ],
      },
    ];
  },
};

export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

Or handle it inside a Route Handler for per-route control:

// app/api/data/route.ts
export async function OPTIONS(request: Request) {
  return new Response(null, {
    headers: {
      'Access-Control-Allow-Origin': 'https://myapp.com',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  });
}

export async function GET(request: Request) {
  return Response.json({ data: 'hello' }, {
    headers: {
      'Access-Control-Allow-Origin': 'https://myapp.com',
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

nginx

server {
  listen 443 ssl;
  server_name api.example.com;

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

    add_header 'Access-Control-Allow-Origin' 'https://myapp.com';
    add_header 'Access-Control-Allow-Credentials' 'true';

    proxy_pass http://localhost:3000;
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ nginx's add_header only applies when the status code is 2xx or 3xx by default. Use add_header ... always; to include headers on error responses too.


Cloudflare Workers

const ALLOWED_ORIGIN = 'https://myapp.com';

export default {
  async fetch(request) {
    const origin = request.headers.get('Origin');

    if (request.method === 'OPTIONS') {
      return new Response(null, {
        status: 204,
        headers: {
          'Access-Control-Allow-Origin': origin === ALLOWED_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',
        },
      });
    }

    const response = await fetch(request);
    const newResponse = new Response(response.body, response);
    newResponse.headers.set('Access-Control-Allow-Origin', 
      origin === ALLOWED_ORIGIN ? origin : '');
    return newResponse;
  },
};
Enter fullscreen mode Exit fullscreen mode

Vercel (vercel.json)

{
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "Access-Control-Allow-Origin", "value": "https://myapp.com" },
        { "key": "Access-Control-Allow-Methods", "value": "GET,POST,PUT,DELETE,OPTIONS" },
        { "key": "Access-Control-Allow-Headers", "value": "Content-Type,Authorization" },
        { "key": "Access-Control-Allow-Credentials", "value": "true" }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The Access-Control-Max-Age Performance Header

Every CORS preflight is an extra OPTIONS request that adds latency before your actual request fires. You can cache the preflight result with Access-Control-Max-Age:

Access-Control-Max-Age: 86400
Enter fullscreen mode Exit fullscreen mode

This tells the browser: "you do not need to send another OPTIONS for 24 hours." Chrome respects a maximum of 7,200 seconds (2 hours); Firefox respects up to 86,400. Set it as high as your browser allows. This is a free performance win that most guides skip.


Private Network Access — The 2026 Gotcha

Since Chrome 98, there is an additional CORS-adjacent policy called Private Network Access (CORS-RFC1918). If a public website (HTTPS) tries to fetch a resource from a private network address (localhost, 192.168.x.x, 10.x.x.x, or a .local hostname), Chrome sends a preflight with an extra header:

Access-Control-Request-Private-Network: true
Enter fullscreen mode Exit fullscreen mode

Your local server must respond with:

Access-Control-Allow-Private-Network: true
Enter fullscreen mode Exit fullscreen mode

If your local dev server does not return this header, Chrome blocks the request even if your normal CORS headers are correct. This catches a lot of developers by surprise when their production frontend cannot talk to a local development backend.

Firefox is implementing the same spec; it will apply broadly by late 2026.

Fix for local dev servers:

  • vite: add server.cors: true or a custom middleware
  • webpack-dev-server: add headers: { 'Access-Control-Allow-Private-Network': 'true' } to devServer.headers
  • Express local: add the header manually in your CORS middleware

The * Wildcard — When It Is Safe and When It Is Dangerous

Access-Control-Allow-Origin: * is fine for:

  • Completely public, read-only APIs (a weather API, a public data endpoint)
  • CDN-hosted static assets
  • Font files

* is never safe for:

  • Any API that reads or mutates user data
  • Any endpoint the client accesses with cookies or an Authorization header
  • Any internal service

The danger: if you return *, any website in the world can read your API responses from inside your users' browsers. An attacker can build a page that silently calls your API using the user's existing session cookies (because the browser attaches them automatically), then exfiltrates the response to the attacker's server.


Common Mistakes

  • Setting CORS headers in the response body instead of HTTP headers. JSON that says { "Access-Control-Allow-Origin": "*" } does nothing. Headers must be in the HTTP response headers, not the body.
  • Only setting CORS on the route, not the OPTIONS handler. The browser's preflight hits your OPTIONS route, which may be returning a 404 or 405 without CORS headers, even though your GET/POST route has them.
  • Setting different origins on different responses. If your CORS middleware sets the origin dynamically, make sure caching layers (CDN, nginx, browser) do not cache a response with the wrong Access-Control-Allow-Origin. Use Vary: Origin to tell caches that the header varies by origin.
  • Missing Vary: Origin when using dynamic origin matching. Without it, a CDN might cache a response that allows https://myapp.com and serve it to a request from a completely different origin, causing inconsistent behaviour.
  • Applying a CORS fix in development only. If your CORS fix is in a .env-gated code path, it will not be present in production. Always verify in a staging environment identical to prod.

Try It In Your Browser

Paste any API URL into the AllDevToolsHub CORS Header Checker to instantly see which CORS headers the server returns, test preflight responses, and validate your Access-Control-Allow-Origin values — no curl command, no Postman, no install required. All requests are made from your browser.


Frequently Asked Questions

Why does my API work in Postman but fail in the browser?

CORS is enforced entirely by the browser, not the server. Postman, curl, and backend services make direct HTTP requests that bypass the Same-Origin Policy. Only browser JavaScript is subject to CORS. If the API works in Postman and fails in your browser's network tab, add Access-Control-Allow-Origin to your server responses.

Can I fix CORS from the frontend?

No. CORS headers must be set by the server. The only thing you can do on the frontend is change how you make the request — for example, routing it through a server-side proxy that adds the correct headers. Browser extensions that "disable CORS" only affect your own browser and are not a solution for users.

What is the difference between simple and non-simple (preflighted) requests?

A "simple" request uses GET, HEAD, or POST with only application/x-www-form-urlencoded, multipart/form-data, or text/plain content types, and no custom headers. Everything else triggers a preflight OPTIONS request. In practice: if you send JSON (Content-Type: application/json) or an Authorization header, expect a preflight.

Why am I getting CORS errors on localhost?

Two common causes: (1) Your backend runs on http://localhost:3001 and your frontend on http://localhost:3000 — those are different origins (different port), so CORS applies. Add http://localhost:3000 to your allowed origins in development. (2) The Private Network Access (CORS-RFC1918) policy — if you are accessing a local server from a public URL, Chrome sends an extra Access-Control-Request-Private-Network: true header and requires Access-Control-Allow-Private-Network: true back.

Should I use Access-Control-Allow-Origin: * in production?

Only for fully public, anonymous, read-only APIs. Never use * on any endpoint that handles authenticated requests, user data, or mutations. Use the exact origin of your frontend app instead, and set Vary: Origin so caches handle the header correctly.


The CORS error pyramid is short: almost every real-world CORS bug is either a missing Access-Control-Allow-Origin header, a * + credentials conflict, or a preflight response that does not list the required method or header. Run the CORS Header Checker to diagnose in seconds, then apply the framework-specific fix above.

For the full list of HTTP response codes your server might return during a failed preflight, see the HTTP Status Code Cheatsheet. For securing your API surface beyond CORS, read HTTP Security Headers: The 2026 Complete Checklist.

Top comments (0)