HomeProjectsBlogContact

How to Fix CORS Errors (and Actually Understand Them)

That red "blocked by CORS policy" message is the browser doing its job. Here is what it protects, why preflight requests fail, and how to fix CORS on the server for good.

How to Fix CORS Errors (and Actually Understand Them)

Sooner or later every web developer meets this line in the console:

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

The usual reaction is to search for a quick fix, paste in mode: 'no-cors' or install a browser extension, and move on. Then it breaks again in production. Let's fix CORS errors properly, and that starts with knowing what CORS is actually doing.

What CORS really is

Browsers enforce the same-origin policy: a page loaded from one origin can't freely read responses from another. An origin is scheme + host + port. So http://localhost:3000 and http://localhost:4000 are different origins, and so are https://example.com and https://api.example.com.

CORS (Cross-Origin Resource Sharing) is how a server opts in and says "that origin is allowed to read my responses." It's nothing more than a handful of HTTP response headers. Which leads to the one sentence worth remembering:

CORS is fixed on the server, not in your frontend code.

Your request usually does reach the server. The browser simply refuses to hand the response to your JavaScript because the server never granted permission. That's also why the same call works fine in Postman or curl. They aren't browsers, so they don't enforce the policy.

Simple requests vs preflight requests

Some requests go straight through. A GET, or a POST with basic headers and a form-style content type, counts as a "simple" request.

Almost everything a modern app sends triggers a preflight instead. Before the real request, the browser fires an OPTIONS request asking for permission. Common triggers:

  • Sending JSON with Content-Type: application/json
  • Adding an Authorization header
  • Using PUT, PATCH or DELETE
  • Any custom header, such as X-Request-Id

If the server doesn't answer that OPTIONS request with a 2xx status and the right headers, the real request never happens. A huge share of "my POST fails with CORS" bugs are really "my server returns 404 or 401 for OPTIONS."

The headers that matter

Header What it does
Access-Control-Allow-Origin Which origin may read the response: one exact origin, or *
Access-Control-Allow-Methods Methods allowed for the real request (sent on the preflight)
Access-Control-Allow-Headers Request headers allowed, e.g. Content-Type, Authorization
Access-Control-Allow-Credentials true when cookies or credentials are included
Access-Control-Max-Age How long the browser may cache the preflight answer, in seconds

Fixing it in common stacks

Express (Node.js) with the cors package:

import express from 'express'
import cors from 'cors'

const app = express()
const allowed = ['http://localhost:3000', 'https://www.yoursite.com']

app.use(cors({
  origin: (origin, cb) => {
    // no Origin header = server-to-server call or a tool like curl
    if (!origin || allowed.includes(origin)) return cb(null, true)
    cb(new Error('Not allowed by CORS'))
  },
  credentials: true,
}))

Register it before your routes and before auth middleware. Otherwise your auth layer rejects the preflight (which never carries your token) and you get a CORS error that's really an auth error.

Next.js route handlers: set the headers on the response and export an OPTIONS handler that returns them as well. Even better, if the frontend and API live in the same Next.js app, call relative URLs like /api/users. No cross-origin request, no CORS question.

Nginx in front of your app: add the headers in the location block and use always, so they're sent on error responses too:

add_header Access-Control-Allow-Origin "https://www.yoursite.com" always;
add_header Vary Origin always;

The credentials trap

If you send cookies (fetch(url, { credentials: 'include' }), or withCredentials: true in Axios), two rules kick in:

  1. Access-Control-Allow-Origin cannot be *. It must echo the exact requesting origin.
  2. The server must send Access-Control-Allow-Credentials: true.

When you echo the origin dynamically, also send Vary: Origin. Without it, a CDN can cache the response for one origin and serve it to another.

"Fixes" that aren't fixes

  • mode: 'no-cors' doesn't bypass anything. It returns an opaque response your code can't read, so things fail silently instead of loudly.
  • CORS-disabling browser extensions only fix your machine. Your users don't have them installed.
  • Access-Control-Allow-Origin: * everywhere is fine for a genuinely public, read-only API. For anything touching user data, allow-list specific origins.

A dev-only shortcut that's actually good

In local development you can skip cross-origin calls entirely with a proxy. Vite's server.proxy or Next.js rewrites forward /api/* from your dev server to your backend. The browser only ever sees one origin. In production, either serve both under one domain or configure real CORS headers.

Five-minute debugging checklist

  1. Open DevTools → Network and find the failing request. Is there an OPTIONS request just before it?
  2. Check the preflight's status code. Anything outside 2xx means a route or middleware rejected it.
  3. Compare the request's Origin header with the response's Access-Control-Allow-Origin. They must match exactly: protocol, host, port, no trailing slash.
  4. Sending cookies? Confirm there's no wildcard and that Allow-Credentials: true is present.
  5. Seeing a 500? A crashing handler can skip the middleware that adds CORS headers, so the real bug hides behind a CORS message. Read the server logs (and if they're cryptic, here's how to read stack traces).

FAQ

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

Postman isn't a browser and doesn't enforce the same-origin policy. The server responds the same way to both; only the browser checks for CORS headers before letting your code read the response.

Can I fix a CORS error from the frontend?

Not really. You can avoid cross-origin requests with a dev proxy or by serving everything from one domain, but the permission itself has to come from the server's response headers.

Does CORS protect my API from attackers?

No. CORS protects users' browsers from malicious sites reading data on their behalf. It does nothing to stop curl, scripts or other servers. You still need authentication, input validation and rate limiting.

Keep reading

More from the workbench

All articles →