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:
- The server is reachable and returning
2xxfor the actual request method (GET,POST, etc.) — verify with curl without CORS headers. - You have access to the server middleware stack or hosting config (Nginx, Express, FastAPI, etc.).
- If a CDN or reverse proxy sits between the browser and origin server, you have access to its response-header policy.
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.
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:
- Whether an
OPTIONSrequest preceded it (indicates a preflight) - The HTTP status code of both requests
- The presence or absence of
Access-Control-Allow-Originin Response Headers
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.
Related
- CORS Error Code Breakdown — parent: maps all browser CORS error codes to their server-side causes
- Understanding Access-Control-Allow-Credentials — credential isolation rules and the wildcard prohibition
- Dynamic Origin Validation Patterns — allowlist-based origin reflection for multi-domain APIs
- Handling Vary: Origin Header Correctly — prevents CDN cache poisoning when origins vary per request
- Why Preflight Requests Use the OPTIONS Method — explains when a preflight fires and what the server must return