Netraptor

What is a CORS error, and how do you fix it?

Updated · 10 min read

A CORS error means the browser blocked your page from reading a response from another origin, because the server did not send an Access-Control-Allow-Origin header that allows your page's origin. You fix it on the server that answers the request, by sending the right CORS headers, or in development by proxying the call through your own origin. The exact Chrome Console message tells you which header is missing or wrong, and the Network panel shows the failing request and its OPTIONS preflight.

Steps

  1. 1

    Read the full Console message

    Open DevTools (F12, or Cmd+Option+I on a Mac) and go to the Console. Chrome logs one line per blocked request, starting Access to fetch at '…' from origin '…' has been blocked by CORS policy:. The text after the colon names the problem.

  2. 2

    Find the failing request in the Network panel

    Open the Network panel, select All in the filter bar and reload. Blocked requests show CORS error in the Status column. Hover over it to see the reason code, such as MissingAllowOriginHeader or PreflightInvalidStatus.

  3. 3

    Check the OPTIONS preflight

    If the request sends JSON, an Authorization header or a method other than GET, HEAD or POST, Chrome sends an OPTIONS preflight first. It appears as its own row with Type preflight. Click the Other filter (or All) to see it, because Fetch/XHR hides it. Check its status code and response headers.

  4. 4

    Send the missing headers from the server

    Add Access-Control-Allow-Origin with your page's exact origin to both the preflight and the real response, plus Access-Control-Allow-Methods and Access-Control-Allow-Headers on the preflight. Use your framework's CORS middleware or the server config shown below.

  5. 5

    Confirm the headers with curl

    Replay the preflight from a terminal with curl -i -X OPTIONS and the same Origin and Access-Control-Request-* headers, then reload the page and check that the Status column shows a real status code.

What is CORS?

Browsers apply the same-origin policy: a script on one origin cannot read responses from another origin. An origin is the scheme, host and port together, so http://localhost:3000 and http://localhost:8080 are different origins, and so are https://example.com and https://api.example.com.

Cross-Origin Resource Sharing, defined in the Fetch standard, is how a server opts in to being read across origins. The browser adds an Origin header to the request. The server answers with Access-Control-Allow-Origin and related headers. If those headers allow the page's origin, the browser hands the response to your JavaScript. If they do not, the browser withholds it and logs a CORS error.

Two facts follow from that, and they explain most confusion:

  • The browser enforces CORS. Postman, curl and server-to-server calls never check these headers, which is why the same API works there and fails in the page.
  • The server decides. Nothing in your frontend code can grant itself permission to read another origin's response.

A simple request (GET, HEAD or POST with only safelisted headers and a Content-Type of text/plain, multipart/form-data or application/x-www-form-urlencoded) goes straight to the server, and the browser checks the response. Anything else triggers a preflight: an OPTIONS request that asks whether the real method and headers are allowed. Sending JSON or an Authorization header is enough to need one. The preflight carries no cookies or credentials. MDN's CORS guide covers the full rules.

What a CORS error looks like in Chrome

Every CORS error in the Chrome Console starts the same way. fetch becomes XMLHttpRequest for XHR and Axios calls, and the text after CORS policy: changes with the cause:

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

In your code, fetch rejects with TypeError: Failed to fetch and Axios reports Network Error. Neither tells you why, because the browser hides CORS details from scripts on purpose. Read the Console line instead. Chrome links it to the request in the Network panel and to an entry in the Issues tab.

When the message starts with Response to preflight request doesn't pass access control check, the OPTIONS preflight failed and the real request was never sent.

Chrome CORS error messages and how to fix each one

These are the messages Chrome prints today, taken from Chromium's CORS error strings, with the fix for each.

  • No 'Access-Control-Allow-Origin' header is present on the requested resource. The response had no allow header at all. Add Access-Control-Allow-Origin: https://your-app.example on the server. If the request returned 401, 404 or 500, your error handler probably skips the CORS middleware, so the real error shows up as a CORS error. Send the header on error responses too.
  • Response to preflight request doesn't pass access control check: It does not have HTTP ok status. The OPTIONS request got a non-2xx status. The usual causes are an auth middleware that rejects the preflight (it has no token) or a router with no OPTIONS handler. Answer OPTIONS with 204 before auth runs.
  • Redirect is not allowed for a preflight request. The server answered the preflight with a 301, 302 or 307. Call the final URL directly. Check for a missing trailing slash, http instead of https, or a login redirect.
  • 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'. You send cookies (credentials: "include" or withCredentials: true) and the server replies with *. Echo the exact origin from an allow list instead, and add Vary: Origin.
  • The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. Same setup, missing header. Add Access-Control-Allow-Credentials: true.
  • Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response. The preflight did not list a header your request sends. Add it to Access-Control-Allow-Headers, for example Authorization, Content-Type.
  • Method PUT is not allowed by Access-Control-Allow-Methods in preflight response. Add the method to Access-Control-Allow-Methods.
  • The 'Access-Control-Allow-Origin' header has a value 'https://app.example.com' that is not equal to the supplied origin. The server allows a different origin. Check scheme, port and www, and add your dev origin to the allow list.
  • The 'Access-Control-Allow-Origin' header contains multiple values '*, *', but only one is allowed. Two layers both add the header, often the app and nginx or an API gateway. Remove one.

MDN's list of CORS errors explains the same causes with Firefox's wording.

How to find the failing request and its preflight in Chrome DevTools

Since Chrome 88, the Network panel shows `CORS error` in the Status column for blocked requests instead of a generic (failed). Hover over it for the reason code. Open DevTools before you reproduce the request, since Chrome only records while DevTools is open, and tick Preserve log if a redirect or reload clears the list. See how to preserve the network log in Chrome.

  • The preflight is a separate OPTIONS row with Type preflight. DevTools files it under Other, so the Fetch/XHR filter hides it. Click All or Other to see it next to the real request.
  • Click the preflight and open Headers. Check the status code first (it should be 200 or 204), then the response headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods and Access-Control-Allow-Headers.
  • Compare them with the request headers on the same row: Origin, Access-Control-Request-Method and Access-Control-Request-Headers. Whatever the request asks for must be allowed in the response.
  • If there is no preflight row, the request was simple or Chrome reused a cached preflight result. Access-Control-Max-Age controls that cache, and Chrome caps it at 7200 seconds. Tick Disable cache and reload to force a new preflight.

Chrome can also override response headers locally. Open a request's Headers tab, edit or add a header under Response Headers, and reload. The first time, Chrome asks you to pick a folder for overrides. That lets you test whether a given Access-Control-Allow-Origin value fixes the error before you change the server.

To test the server outside the browser, replay the preflight with curl. Right-click the request, choose Copy > Copy as cURL, and edit it into an OPTIONS call:

curl -i -X OPTIONS 'https://api.example.com/orders' \
  -H 'Origin: http://localhost:5173' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization, content-type'

curl does not enforce CORS, so it shows exactly what the server sends back. If the Access-Control-* headers are missing here, the fix belongs on the server.

How to fix a CORS error on the server

Allow specific origins, answer the preflight, and send the headers on every response, including errors. In Express, the `cors` middleware does all three when you mount it before your routes and auth:

import express from "express";
import cors from "cors";

const app = express();

app.use(cors({
  origin: ["http://localhost:5173", "https://app.example.com"],
  credentials: true, // only if the browser sends cookies
}));

app.use(authMiddleware); // after cors, so preflights are answered first

With an origin list, cors echoes the matching origin, adds Vary: Origin and replies to OPTIONS with 204. By default it allows the headers the preflight asks for.

In a Next.js App Router route handler, export an OPTIONS function and add the same headers to your other methods:

// app/api/orders/route.ts
const ALLOWED = new Set(["http://localhost:5173", "https://app.example.com"]);

function corsHeaders(origin: string | null): Record<string, string> {
  return {
    ...(origin && ALLOWED.has(origin) ? { "Access-Control-Allow-Origin": origin } : {}),
    "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization",
    Vary: "Origin",
  };
}

export function OPTIONS(request: Request) {
  return new Response(null, { status: 204, headers: corsHeaders(request.headers.get("origin")) });
}

export async function GET(request: Request) {
  return Response.json({ orders: [] }, { headers: corsHeaders(request.headers.get("origin")) });
}

For many routes, set the headers once in `proxy.ts` (called middleware.ts before Next.js 16), which the Next.js docs show for both simple and preflighted requests.

In nginx, answer OPTIONS directly and use always so the header is also sent on 4xx and 5xx responses:

location /api/ {
    if ($request_method = OPTIONS) {
        add_header Access-Control-Allow-Origin "https://app.example.com" always;
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
        add_header Access-Control-Max-Age 7200 always;
        return 204;
    }
    add_header Access-Control-Allow-Origin "https://app.example.com" always;
    add_header Vary Origin always;
    proxy_pass http://backend;
}

Set CORS in one layer only. If both nginx and the app add Access-Control-Allow-Origin, Chrome rejects the duplicate header.

How to fix a CORS error on localhost

A React or Vue app on localhost:5173 calling an API on localhost:8080 is a cross-origin request, because the ports differ. In development you can avoid CORS entirely by having the dev server forward API calls, so the browser only ever talks to its own origin.

With Vite, add a `server.proxy` entry and call relative URLs such as fetch("/api/orders"):

// vite.config.ts
import { defineConfig } from "vite";

export default defineConfig({
  server: {
    proxy: {
      "/api": { target: "http://localhost:8080", changeOrigin: true },
    },
  },
});

With Next.js, a rewrite does the same job, and it works in production too:

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

The Vite proxy only runs in the dev server. When you deploy the frontend and API on different origins, the API still has to send CORS headers, or your production server has to proxy the same path.

Why mode: 'no-cors' does not fix a CORS error

fetch(url, { mode: "no-cors" }) makes the error go away from the Console and leaves you with nothing usable. The browser returns an opaque response: response.type is "opaque", status is 0, the headers are empty and the body cannot be read.

const res = await fetch("https://api.example.com/orders", { mode: "no-cors" });
console.log(res.type, res.status); // "opaque" 0
await res.text(); // ""

no-cors also limits the request. Only GET, HEAD and POST are allowed, and headers outside the safelist, such as Authorization or Content-Type: application/json, are dropped without a warning. It exists for fire-and-forget requests and for caching opaque resources in a service worker. For an API call whose response you need, the server has to allow your origin.

Why disabling web security or a CORS extension is not a fix

Starting Chrome with --disable-web-security --user-data-dir=/tmp/chrome-dev, or installing an extension that injects Access-Control-Allow-Origin: * into every response, makes the error disappear on your machine only. Your users' browsers still block the request, so the bug ships.

Both also weaken your browser. With web security off, any site you open in that profile can read responses from any other origin, including sites where you are logged in. If you need a quick test, use DevTools header overrides on the one request, then fix the server.

Debugging CORS errors with Netraptor

Netraptor is a Chrome extension with its own DevTools panel. In live capture it lists what Chrome DevTools records for the inspected tab, so CORS preflights appear too. It has no special CORS label, so use these filters and actions instead:

  • Filter method:OPTIONS to list the preflights. They sit under the Other type chip, as in Chrome, so pick Other or All rather than Fetch/XHR.
  • Filter is:error to list failed calls and 4xx/5xx responses together. Requests that ended with status 0 show Failed in the Status column. Saved history records what the page's own fetch and XHR saw, so there a CORS-blocked call shows Failed and a no-cors response shows Opaque.
  • With the Fetch/XHR chip on, filter -has-response-header:access-control-allow-origin to find API responses that came back without the header, or is:cors-any to find the ones that allow every origin with *.
  • Right-click a request and choose Copy > Copy as cURL (bash) to replay it in a terminal, where CORS does not apply. Add -i to see the response headers the server really sends.

Resend and Edit and resend as fetch run the request inside the inspected page, so they hit the same CORS rules as your app. Use them to check a server fix, and curl to check the server on its own. To find every endpoint your app calls, see how to find the API endpoints of a web app.

Frequently asked questions

Is strict-origin-when-cross-origin a CORS error?
No. strict-origin-when-cross-origin is a Referrer-Policy value, and it has been Chrome's default since Chrome 85. It controls how much of the page URL goes in the Referer header. DevTools shows it under General on every request, so it sits next to CORS errors without causing them.
Why does my API work in Postman or curl but not in the browser?
CORS is enforced by browsers only. Postman and curl send the request and show the response without checking Access-Control-Allow-Origin. The browser makes the same request and then refuses to give the response to your page because the server did not allow your origin.
How do I fix a CORS error in React?
React itself has no CORS setting. Fix the API server so it allows your app's origin, or in development proxy /api through your dev server with Vite's server.proxy or a Next.js rewrite, and call relative URLs like /api/orders.
Can I fix a CORS error from the frontend?
Only by not making a cross-origin call. Route the request through a proxy on your own origin, such as a dev server proxy, a rewrite or your own backend. Headers you set in fetch cannot grant permission. Access-Control-Allow-Origin is a response header that the server sends.
What does Access-Control-Allow-Origin: * mean, and is it safe?
* lets any origin read the response, as long as the request does not include credentials. That suits public, unauthenticated data. For APIs that use cookies or user tokens, allow a list of known origins instead. Browsers reject * on requests that send cookies anyway.
Does a CORS error mean the request never reached the server?
Not always. A simple request, or the real request after a passing preflight, reaches the server and runs. The browser only blocks the page from reading the response. If the preflight fails, the real request is never sent. Check the server logs before you retry a POST.