/errors/cors/request-header-field-not-allowed-by-access-control-allow-headers
CORS: Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response
- tool
- CORS
- versions
- Fetch Standard, Chrome, Edge, Firefox, Safari
- os
- any
The browser sent a preflight (OPTIONS) request announcing, in Access-Control-Request-Headers, the headers the real request will carry. The server's answer did not include authorization (or the header named in the message) in Access-Control-Allow-Headers, so the browser did not send the real request. Add the header to Access-Control-Allow-Headers on the OPTIONS response, for example Access-Control-Allow-Headers: Content-Type, Authorization.
Reproduce it
The API allows only Content-Type; the page sends a bearer token. Microsoft Edge runs headless through playwright-core:
npm init -y >/dev/null
npm install --silent --no-audit --no-fund playwright-core@1 >/dev/null 2>&1
cat > preflight.js <<'EOF'
const http = require("http");
const { chromium } = require("playwright-core");
const allowHeaders = process.argv[2];
const api = http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5036");
if (req.method === "OPTIONS") {
console.log("preflight asks for:", req.headers["access-control-request-headers"]);
res.setHeader("Access-Control-Allow-Methods", "GET, POST");
res.setHeader("Access-Control-Allow-Headers", allowHeaders);
res.writeHead(204); return res.end();
}
res.end('{"ok":true}');
}).listen(5037, "127.0.0.1");
const web = http.createServer((req, res) => res.end("<!doctype html><title>app</title>")).listen(5036, "127.0.0.1");
(async () => {
const browser = await chromium.launch({ channel: "msedge", headless: true });
const page = await browser.newPage();
const errors = [];
page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });
await page.goto("http://127.0.0.1:5036/");
const result = await page.evaluate(() => fetch("http://127.0.0.1:5037/items", {
method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer demo" }, body: "{}",
}).then((r) => "HTTP " + r.status, (e) => e.name + ": " + e.message));
console.log(`Allow-Headers "${allowHeaders}":`, result);
await new Promise((r) => setTimeout(r, 300));
for (const e of errors.filter((e) => e.includes("CORS"))) console.log(e);
await browser.close(); api.close(); web.close();
})();
EOF
node preflight.js "Content-Type"
node preflight.js "Content-Type, Authorization"The first run prints what the preflight asked for and the exact console message; with Authorization added to the list the preflight passes and the POST is sent. Header names are case-insensitive; the browser announces them in lower case.
Which headers need to be allowed
Only CORS-safelisted request headers skip this check: Accept, Accept-Language, Content-Language, Range (simple ranges), and Content-Type with one of application/x-www-form-urlencoded, multipart/form-data or text/plain. Everything else must be listed, including:
| Header | Common source |
|---|---|
| Authorization | Bearer tokens, Basic auth |
| Content-Type: application/json | fetch or axios with a JSON body |
| X-Requested-With | jQuery and some legacy libraries |
| X-Api-Key, X-CSRF-Token | API keys, CSRF protection |
| traceparent, sentry-trace, baggage | Tracing and monitoring SDKs added to every request |
Server examples
// Express with the cors package
app.use(cors({ origin: "https://app.example.com", allowedHeaders: ["Content-Type", "Authorization"] }));# nginx, inside the location that answers the 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 "Content-Type, Authorization" always;
add_header Access-Control-Max-Age 600 always;
return 204;
}Access-Control-Allow-Headers: * allows any header for requests without credentials, but Authorization must still be named explicitly.
FAQ
Why does the request work in curl or Postman but not in the browser?
CORS is enforced only by browsers. curl and Postman send the request directly and never run a preflight, so they cannot show this error; test the OPTIONS response instead.
Does the header name have to match the case the browser uses?
No. Header names are case-insensitive, so Authorization in Access-Control-Allow-Headers covers the lower-case authorization the browser announces.
Can I avoid the preflight altogether?
Only by sending nothing but safelisted headers and methods, for example a form-encoded POST without Authorization. Serving the frontend and the API from the same origin through a reverse proxy also removes CORS from the picture.