devkult_
tools29converters34
home/blog/cors-errors-explained
httpcorssecurity

CORS Errors Explained: How to Fix "No 'Access-Control-Allow-Origin' Header"

You call your API from the browser and the console fills with red: blocked by CORS policy. The confusing part is that the request usually did reach your server — often it even succeeded. CORS doesn't stop the request; it stops your JavaScript from reading the response. That's also why the same call works fine in curl or Postman: CORS is enforced by browsers, and only browsers.

What counts as cross-origin

An origin is the scheme, host and port together. These are all different origins:

  • https://app.example.com and https://api.example.com — different host
  • http://localhost:3000 and http://localhost:8080 — different port
  • http://example.com and https://example.com — different scheme

When a page on one origin fetches from another, the browser asks the server for permission through response headers, chiefly Access-Control-Allow-Origin.

Simple requests and preflights

Some requests are sent straight away, and the browser checks the response headers afterwards. Others trigger a preflight: an automatic OPTIONS request that asks whether the real request is allowed. A preflight happens when, among other things, the request:

  • uses a method other than GET, HEAD or POST,
  • sends a header outside a short safe list — Authorization is the classic one, or
  • sends Content-Type: application/json (only text/plain, multipart/form-data and application/x-www-form-urlencoded are exempt).

So almost every real API call — JSON body, bearer token — is preflighted. The server must answer that OPTIONS request successfully, with the right headers, before the browser sends the real one.

The common errors, and their fixes

"No 'Access-Control-Allow-Origin' header is present on the requested resource." The response didn't include the header. Add it, with the calling origin as the value:

Access-Control-Allow-Origin: https://app.example.com
Vary: Origin

Vary: Origin matters when you choose the value per request: it stops a CDN from serving one origin's response to another.

"Response to preflight request doesn't pass access control check: It does not have HTTP ok status." Your server answered the OPTIONS preflight with an error — often 401 because auth middleware ran first, or 404/405 because no route handles OPTIONS. Answer preflights before authentication, with 204 No Content and the CORS headers.

"Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response." List every custom header the client sends:

Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Methods: GET, POST, PUT, DELETE

"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'." Requests that send cookies (credentials: "include") can't use *. Return the specific origin plus Access-Control-Allow-Credentials: true.

"Redirect is not allowed for a preflight request." The preflight hit a redirect — commonly a trailing-slash or http→https rule. Call the final URL directly.

Server examples

Express, with the cors package and an explicit allow-list:

import cors from "cors";

app.use(cors({
  origin: ["https://app.example.com", "http://localhost:5173"],
  credentials: true,
  maxAge: 600, // cache the preflight for 10 minutes
}));

A Next.js route handler answers preflights by exporting OPTIONS:

const CORS = {
  "Access-Control-Allow-Origin": "https://app.example.com",
  "Access-Control-Allow-Methods": "GET, POST",
  "Access-Control-Allow-Headers": "Authorization, Content-Type",
};

export function OPTIONS() {
  return new Response(null, { status: 204, headers: CORS });
}

export async function POST(request: Request) {
  return Response.json({ ok: true }, { headers: CORS });
}

nginx in front of an API:

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" always;
    add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
    add_header Access-Control-Max-Age 600 always;
    return 204;
  }
  add_header Access-Control-Allow-Origin "https://app.example.com" always;
  proxy_pass http://127.0.0.1:3000;
}

The always flag makes nginx add the header to error responses too — without it, a 500 from your API shows up in the browser as a CORS error, which hides the real problem.

What not to do

  • Don't reflect any origin with credentials. Echoing whatever Origin arrives, together with Access-Control-Allow-Credentials: true, lets any website read your users' data with their cookies. Check origins against an allow-list.
  • Don't use mode: "no-cors". It makes the error disappear by giving you an opaque response you can't read. The request "works"; your code gets nothing.
  • Don't fight CORS in development. Use your dev server's proxy (Vite's server.proxy, Next.js rewrites) so the browser sees one origin.

Testing a preflight with curl

curl doesn't enforce CORS, but it can show you exactly what the browser would see:

curl -i -X OPTIONS https://api.example.com/items \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization, content-type"

Look for a 2xx status and the three Access-Control-Allow-* headers. To check what a list of endpoints returns, paste them into the HTTP Status Checker.

One last tip: a preflight is cached for as long as Access-Control-Max-Age says — up to 2 hours in Chrome and 24 hours in Firefox — so after changing CORS headers, test in a fresh private window.

For each header in detail, see the reference pages for Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials and Origin.

More from the blog