CORS error: what it means and how to fix it

You call an API from your web page and the console says the request was blocked by CORS policy. The request often reached the server just fine; the browser simply won't let your page read the answer. Here's why, and the fixes in order of how permanent they are.

Looking up one specific message, such as does not have HTTP ok status or must not be the wildcard '*'? Every variant of “blocked by CORS policy”, with its fix.

On this page
  1. What the error means
  2. Why it happens on localhost
  3. Fix 1: send the right headers from the server
  4. Fix 2: handle the preflight (OPTIONS) request
  5. Fix 3: use a dev proxy
  6. For testing only: add the headers in your browser
  7. Why not just disable web security?

What the error means

A typical message in Chrome looks like this:

Access to fetch at 'http://localhost:8080/api/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the
requested resource.

Browsers follow the same-origin policy: a page may only read responses from its own origin, which is the scheme, host and port together. To let another origin read a response, the server has to say so with Access-Control-Allow-* headers. That opt-in system is CORS, Cross-Origin Resource Sharing. Tools like curl and Postman don't enforce it, which is why the same request works there.

Why it happens on localhost

http://localhost:3000 and http://localhost:8080 are different origins because the ports differ. So a front end on one dev server calling an API on another is cross-origin, even on the same machine. 127.0.0.1 and localhost are different origins too.

Fix 1: send the right headers from the server

This is the real fix, because it works for every user. The API must answer with at least:

Access-Control-Allow-Origin: http://localhost:3000

In Node with Express, the cors package does it:

import cors from "cors";
app.use(cors({ origin: ["http://localhost:3000", "https://app.example.com"] }));

If the request sends cookies or an Authorization header with credentials: "include", the server must name the exact origin, not *, and also send Access-Control-Allow-Credentials: true.

Fix 2: handle the preflight (OPTIONS) request

For requests with methods like PUT or DELETE, a JSON Content-Type, or custom headers, the browser first sends an OPTIONS "preflight". The server must answer it with a 2xx status and:

Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

A common trap: the server's login check rejects the OPTIONS request with 401 or 404 because it carries no token. Let OPTIONS through before authentication.

Fix 3: use a dev proxy

During development you can avoid cross-origin calls entirely by letting your dev server forward API calls, so the browser only ever talks to one origin. In Vite:

// vite.config.js
export default { server: { proxy: { "/api": "http://localhost:8080" } } };

Then call fetch("/api/users") instead of the full address.

For testing only: add the headers in your browser

Sometimes you can't change the server: a third-party API, a staging server that isn't yours, or you just want to confirm CORS is the only problem. A header extension can add the Access-Control-Allow-* headers to responses in your own browser. Nobody else is affected, so this never replaces Fix 1.

HeaderForge: "Allow CORS for this site"

One click adds the CORS headers for the site you're on, only for requests made by that site's pages, grouped under one switch so you can turn it off again. It's free, and it has no analytics or ads.

Headers can't change a status code. If the server rejects the preflight with an error, no extension can fix that. HeaderForge tells you so rather than pretending.

Get HeaderForge for Chrome

Why not just disable web security?

You can start Chrome with --disable-web-security, but only together with a separate throwaway profile, for example --user-data-dir=/tmp/chrome-dev. It switches off protections for every site in that window, including the ones you're logged in to. If you use it, never browse normally in that window. A per-site header rule is much narrower.

Read your own error

Chrome’s CORS messages all look alike, and the useful part is one clause in the middle. Paste the whole red line from your console and this will say which of the causes above it is. It runs in your browser; nothing is sent anywhere.

Quick checklist

More on this topic: Access-Control-Allow-Origin: values, multiple origins and server examples · What is CORS? Cross-Origin Resource Sharing, in plain words · Preflight request (CORS OPTIONS): when it happens and how to fix it · How to disable CORS in Chrome (and the safer way to test)