Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 5 min read

What a CORS error actually means, and how I fix each Chrome message

TL;DR: A CORS error means the browser refused to give your page a response from another origin, because the server didn't send an Access-Control-Allow-Origin header that allows you. The fix lives on the server, or in a d

TL;DR: A CORS error means the browser refused to give your page a response from another origin, because the server didn't send an Access-Control-Allow-Origin header that allows you. The fix lives on the server, or in a dev proxy so the browser only talks to its own origin. The text after has been blocked by CORS policy: in the Console tells you which header to fix. If the request sends JSON or an Authorization header, look at the OPTIONS preflight first. The Network panel files it under Other, so the Fetch/XHR filter hides it.

The 30-second version of CORS

  • An origin is scheme + host + port. localhost:5173 and localhost:8080 are different origins.
  • The browser enforces CORS. Postman and curl don't, which is why "it works in Postman" tells you nothing.
  • The server opts in by sending Access-Control-Allow-Origin and friends. Your frontend can't grant itself access.
  • JSON bodies, Authorization headers and methods other than GET/HEAD/POST trigger a preflight: an OPTIONS request, sent without cookies or tokens, asking whether the real request is allowed.

Read the message, not the TypeError

In code you only get TypeError: Failed to fetch (or Axios's Network Error). The browser hides the reason from scripts on purpose. The Console has the real message:

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.

Here's what I do for each ending Chrome prints. The strings come from Chromium's cors_error_string.cc:

Message ends with Fix
No 'Access-Control-Allow-Origin' header is present on the requested resource. Send the header. If the response was a 401/404/500, your error path probably skips the CORS middleware.
It does not have HTTP ok status. (preflight) The OPTIONS request got a non-2xx response. Usually auth middleware rejecting a preflight that has no token. Answer OPTIONS with 204 before auth.
Redirect is not allowed for a preflight request. The preflight hit a 301/302/307. Use the final URL (trailing slash, https, login redirect).
...must not be the wildcard '*' when the request's credentials mode is 'include'. You send cookies. Echo the exact origin, add Vary: Origin.
...'Access-Control-Allow-Credentials' header in the response is '' which must be 'true'... Add Access-Control-Allow-Credentials: true.
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response. Add the header to Access-Control-Allow-Headers.
Method PUT is not allowed by Access-Control-Allow-Methods in preflight response. Add the method to Access-Control-Allow-Methods.
...has a value '...' that is not equal to the supplied origin. Allow list has the wrong origin. Check port, scheme, www.
...contains multiple values '*, *', but only one is allowed. Both nginx and the app add the header. Remove one.

Finding the preflight in DevTools

  1. Open DevTools before reproducing. Tick Preserve log if a redirect clears the list.
  2. In the Network panel, pick All instead of Fetch/XHR.
  3. The blocked request shows CORS error in the Status column (Chrome 88+). Hover it for the reason code, e.g. PreflightInvalidStatus.
  4. The preflight is its own OPTIONS row with Type preflight. Check its status, then compare Access-Control-Request-Method / -Headers in the request with Access-Control-Allow-* in the response.
  5. No preflight row? The request was simple, or Chrome used a cached result (Access-Control-Max-Age, capped at 7200 s in Chrome). Tick Disable cache and retry.

Then I replay the preflight with curl, which shows exactly what the server returns:

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'

To test a fix before touching the server, Chrome 113+ lets you override response headers locally: Network > Headers > Response Headers, edit the value, reload.

Server fixes

Express, mounted before auth:

import cors from "cors";

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

Next.js App Router route handler:

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

const corsHeaders = (origin: string | null): Record<string, string> => ({
  ...(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 const OPTIONS = (req: Request) =>
  new Response(null, { status: 204, headers: corsHeaders(req.headers.get("origin")) });

export const GET = async (req: Request) =>
  Response.json({ orders: [] }, { headers: corsHeaders(req.headers.get("origin")) });

For lots of routes, do it once in proxy.ts (middleware.ts before Next 16).

nginx. The always matters, because without it your 401s and 500s go out without the header and surface as CORS errors:

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;
        return 204;
    }
    add_header Access-Control-Allow-Origin "https://app.example.com" always;
    add_header Vary Origin always;
    proxy_pass http://backend;
}

On localhost, skip CORS with a proxy

Vite:

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

Next.js:

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

Call fetch("/api/orders") and the browser never makes a cross-origin request. Remember the Vite proxy is dev-only.

Things that look like fixes but aren't

  • mode: "no-cors" gives you an opaque response: status 0, no headers, empty body. It also silently drops Authorization and Content-Type: application/json.
  • --disable-web-security or a "CORS unblock" extension fixes your browser only. Your users still get the error, and your profile is now readable by any site.
  • strict-origin-when-cross-origin is Chrome's default Referrer-Policy (since Chrome 85). It shows up in every request's Headers tab and has nothing to do with CORS.

A faster way to find the preflights

I work on Netraptor, a Chrome extension with its own DevTools panel. In live capture it lists the same requests DevTools records, preflights included, so I filter method:OPTIONS to see every preflight on the page, or is:error for failed calls and 4xx/5xx together. Right-click > Copy > Copy as cURL (bash) gives me the call to replay in a terminal, where CORS doesn't apply.

The full guide, with steps and FAQ: What is a CORS error, and how do you fix it?

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.