How to Fix a Missing Access-Control-Allow-Origin Header

When a browser blocks a cross-origin request with the message No 'Access-Control-Allow-Origin' header is present on the requested resource, the server either never emitted the header, emitted it on the wrong response phase, or a proxy stripped it before delivery. This page walks through the exact diagnostic and repair sequence.

This page is part of the CORS Error Code Breakdown cluster, which maps browser-reported CORS errors to their server-side causes.

Exact Browser Error This Page Resolves

Access to XMLHttpRequest at 'https://api.example.com/v1/data'
from origin 'https://app.example.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Chrome also surfaces this as:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the
remote resource at https://api.example.com/v1/data.
(Reason: CORS header 'Access-Control-Allow-Origin' missing)

Both messages mean the same thing: the HTTP response that the browser received contained no Access-Control-Allow-Origin header, so the browser withheld the response body from JavaScript.

Root Cause

The WHATWG Fetch specification (§3.2) requires every cross-origin response to carry an Access-Control-Allow-Origin header whose value either matches the Origin header sent by the browser or is * (for non-credentialed requests). If the header is absent, the browser’s CORS algorithm reaches the “CORS check fails” branch and raises a network error — it does not fall back or retry. The server returned a valid HTTP response; the browser deliberately discards it. Understanding how browsers evaluate the same-origin policy explains why the check happens on the client, not the server.

Prerequisite State

Before applying any fix, confirm:


Diagnostic: Determine Where the Header Is Missing

The error has three distinct causes, each with a different fix. The SVG below maps the decision tree.

Diagnosing a missing Access-Control-Allow-Origin header A decision tree showing three diagnostic branches: server never sends the header, middleware order strips it, and a proxy removes it before the browser receives it. Browser reports header missing Does curl OPTIONS show the header? (curl -X OPTIONS with Origin header) Yes Proxy or CDN is stripping it Add always flag in Nginx / configure CDN pass-through No Server never emits header Add CORS middleware or Nginx add_header Partial Is header on OPTIONS but not on the actual GET/POST? Yes Middleware runs only for OPTIONS. Move cors() before route handlers.

Step-by-Step Fix

Step 1 — Reproduce and capture the exact error

Open DevTools, go to the Network tab, enable Preserve log, and trigger the cross-origin request. Locate the failed request and note:

Step 2 — Run a curl preflight to isolate the server

curl -sv -X OPTIONS https://api.example.com/v1/data \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization" \
  2>&1 | grep -E "< HTTP|Access-Control"

A correct response emits:

< HTTP/2 204
< access-control-allow-origin: https://app.example.com
< access-control-allow-methods: POST, GET, OPTIONS
< access-control-allow-headers: Content-Type, Authorization
< vary: Origin

If no Access-Control-Allow-Origin appears, the server is not emitting it at all — proceed to Step 3. If it appears in curl but not in the browser, a proxy is stripping it — skip to Step 5.

Step 3 — Add CORS middleware (Express / Node.js)

Install the cors package and place it as the first middleware, before any route definition or error handler:

const express = require('express');
const cors = require('cors');

const app = express();

const ALLOWED_ORIGINS = [
  'https://app.example.com',
  'https://admin.example.com',
];

// Must be first — before routes and error handlers
app.use(cors({
  origin: function (origin, callback) {
    // Allow server-to-server or same-origin tool calls (no Origin header)
    if (!origin || ALLOWED_ORIGINS.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Origin not permitted'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
}));

// Routes come after
app.get('/v1/data', (req, res) => res.json({ ok: true }));

Step 4 — Add CORS headers in Nginx (static or reverse-proxy config)

Use the map directive to validate the requesting origin before injecting headers. Do not use if blocks for header assignment — they are unreliable when combined with proxy_pass:

map $http_origin $cors_origin {
  default          "";
  ~^https://(app|admin)\.example\.com$ $http_origin;
}

server {
  listen 443 ssl;
  server_name api.example.com;

  location /v1/ {
    # always ensures header is sent on error responses too
    add_header Access-Control-Allow-Origin  $cors_origin always;
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
    add_header Vary                          Origin always;

    if ($request_method = OPTIONS) {
      add_header Access-Control-Max-Age 86400;
      return 204;
    }

    proxy_pass http://backend_upstream;
  }
}

The always flag ensures the header is attached even when the upstream returns a 4xx or 5xx status.

Step 5 — Configure FastAPI / Python

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

ALLOWED_ORIGINS = [
    "https://app.example.com",
    "https://admin.example.com",
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=ALLOWED_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["Content-Type", "Authorization"],
)

@app.get("/v1/data")
async def get_data():
    return {"ok": True}

Verification

After applying the fix, confirm the header is present at both the preflight and actual-request stages.

curl check — preflight:

curl -sv -X OPTIONS https://api.example.com/v1/data \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" 2>&1 | grep -i "access-control"

curl check — actual request:

curl -sv -X POST https://api.example.com/v1/data \
  -H "Origin: https://app.example.com" \
  -H "Content-Type: application/json" \
  -d '{}' 2>&1 | grep -i "access-control"

Both must return access-control-allow-origin: https://app.example.com.

DevTools verification checklist:


Security Boundary Note

Do not set Access-Control-Allow-Origin: * when the request carries credentials (cookies, Authorization headers, or TLS client certificates). The WHATWG Fetch spec §3.2.5 explicitly forbids browsers from exposing a credentialed response when the origin value is *. Any middleware that sets the wildcard unconditionally while also setting Access-Control-Allow-Credentials: true will cause browsers to silently block the response — and this mis-configuration is nearly invisible without a deliberate test. Reflect the exact origin from a server-side allowlist instead, as shown in dynamic origin validation patterns.


Common Mistakes

Mistake Technical Impact Fix
CORS middleware placed after route definitions Route handlers execute and send responses before CORS headers are attached; the browser sees no header Move app.use(cors(...)) to the top of the middleware stack, before any app.get/post/use route
Nginx add_header without always flag Headers are omitted on 4xx/5xx responses from the upstream; CORS errors appear only on server-side failures Add always to every add_header directive in the CORS block
Wildcard origin * with credentials: true Browsers reject the response; JavaScript receives a network error with no useful detail Replace * with exact origin reflection; omit Access-Control-Allow-Credentials for public APIs
Missing Vary: Origin on multi-origin allowlists CDN or shared caches serve a response with one origin’s header to a different origin, causing intermittent CORS failures Append Vary: Origin on every response that conditionally sets Access-Control-Allow-Origin — see handling Vary: Origin correctly

FAQ

Why does the browser report a missing header when the server actually sends it?

A proxy, CDN, or misconfigured middleware layer may strip or override the header before the response reaches the browser. Duplicate header injection — for example a framework adding the header and Nginx also adding it — also triggers a browser parsing failure, because the spec requires exactly one value. Audit each network layer with curl to find where the header disappears.

How do I fix the missing header only on preflight OPTIONS requests but not on the actual request?

The CORS middleware must run for OPTIONS requests before any route handler or error handler executes. Most frameworks short-circuit OPTIONS early. Place cors() or equivalent at the very top of the middleware stack and confirm the OPTIONS handler returns a 204 with the header attached, independently of the main route logic.

Can I use Access-Control-Allow-Origin: * during development to bypass the error?

Only for requests that carry no credentials. If the request uses withCredentials: true or sends cookies, browsers reject the wildcard response regardless of the server’s intent. Use exact origin reflection even in development when credentials are involved, to avoid a false sense of a working configuration.