CORS Works on Localhost, Fails in Prod — The Real Checklist

(Updated: July 16, 2026 ) CORS fetch webdev

r/webdev classic: “API works in Postman, fails in browser with CORS error.” Top comment: “CORS.” OP: “But it works on localhost.” That’s exactly the point — localhost is a different origin from https://app.example.com, and your dev setup probably proxies API calls through Vite/webpack while production hits a different subdomain cold.

  • Access-Control-Allow-Origin: * with credentials — Browsers reject credentials: 'include' with wildcard origin. You must echo a specific origin.
  • Fixing CORS in React — No amount of axios config adds ACAO headers. Server or edge must emit them.
  • “Disable CORS in Chrome” — Dev-only flag; users won’t disable it. Also doesn’t help your users.
  • Assuming GET never preflights — Custom headers (Authorization, X-Whatever) trigger preflight even on GET.

Simple vs preflighted requests

Simple (often no preflight):

  • Methods: GET, HEAD, POST
  • Headers: Accept, Accept-Language, Content-Language, Content-Type (limited values)
  • Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain

Triggers preflight (OPTIONS first):

  • Content-Type: application/json
  • Custom headers like Authorization: Bearer …
  • Methods: PUT, PATCH, DELETE

Both the OPTIONS response and the actual request response need correct CORS headers.

Localhost vs production comparison

Local devProduction
Frontend originhttp://localhost:5173https://app.example.com
APIOften proxied (same origin in browser)https://api.example.com
CookiesSameSite=Lax surprisesSecure, domain-scoped
TLSHTTP OK locallyHTTPS required for many APIs

Dev proxy makes browser think API is same-origin — CORS never runs. Ship without proxy config → first real cross-origin request explodes.

Minimal server fix (Express example)

app.use((req, res, next) => {
  const allowed = ["https://app.example.com", "http://localhost:5173"];
  const origin = req.headers.origin;
  if (allowed.includes(origin)) {
    res.setHeader("Access-Control-Allow-Origin", origin);
    res.setHeader("Access-Control-Allow-Credentials", "true");
  }
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
  if (req.method === "OPTIONS") return res.sendStatus(204);
  next();
});

Cloudflare Workers, nginx, API Gateway — same headers, different syntax.

Debug checklist (print this)

  1. Network tab → failed request → Response headers. Is access-control-allow-origin present and matching your page origin exactly (scheme + host + port)?
  2. Preflight — Is there an OPTIONS request? Does it return 204/200 with same ACAO?
  3. Credentials — If using cookies, ACAO cannot be *. Need Allow-Credentials: true and specific origin.
  4. Redirect — 301 on API URL during preflight? Some clients lose CORS headers on redirect chains.
  5. Proxy/CDN — Cloudflare or nginx stripping headers? Check origin response vs edge response.
  6. Environment — Staging API allowlist missing staging frontend URL?

curl reproduction

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

Look for Access-Control-Allow-Origin in output — if missing, browser will block regardless of Postman success.

When CORS isn’t the bug

  • 401/403 masked as CORS — Sometimes no CORS headers on error responses; browser reports CORS instead of auth failure. Fix error handler to attach CORS headers on 4xx/5xx too.
  • Mixed content — HTTPS page calling HTTP API — different error, but same confusion.
  • DNS / SSL — Request never reaches app; still looks like “network error.”

Use json-formatter to pretty-print API JSON responses once CORS is fixed — validate payload shape separately from transport headers.

TL;DR

CORS is the browser enforcing cross-origin rules. Localhost often hides it behind a dev proxy. Production needs explicit ACAO for your real origin, correct preflight handling, and credentials rules. Fix the server, not the fetch wrapper.