Request header field <header> is not allowed by Access-Control-Allow-Headers

Quick answer

A specific header your request sends (commonly Authorization, X-API-Key, or a custom X-* header) isn't on the server's preflight allow-list. Add that exact header name to Access-Control-Allow-Headers in the server's OPTIONS response for that route.

The exact error

Access to fetch at 'https://api.example.com/data' from origin
'https://app.example.com' has been blocked by CORS policy:
Request header field x-api-key is not allowed by
Access-Control-Allow-Headers in preflight response.

Unlike the broader preflight-failure error covered on its own reference page, this specific message names the exact offending header — in this example, x-api-key — which means the preflight OPTIONS request did succeed and did return CORS headers, but its Access-Control-Allow-Headers list simply didn't include this particular one.

How to read this error in 30 seconds

  1. The header name is right there in the message — note it exactly, including case.
  2. Open DevTools → Network tab, find the OPTIONS preflight request, check its Access-Control-Allow-Headers response header value.
  3. Confirm whether the named header is missing entirely, or present with a typo/different casing than what your client actually sends.
  4. Check whether the header is added conditionally in your client code (e.g. only when logged in) — this explains intermittent failures.

What the browser is actually comparing

Headers the request sends Access-Control-Allow-Headers authorization x-api-key authorization (not listed) match missing One missing header blocks the entire request — there is no partial success.

A pure list-membership check: every header name in the request must appear in the server's allow-list. If even one is missing, the whole request is blocked.

This is a pure list-membership check, nothing more: for every header name the browser sees in your actual request, it looks for that exact name somewhere in the preflight's Access-Control-Allow-Headers value. One missing name fails the whole request — there's no partial success, and no way for the browser to know the server "would have been fine with it" if it isn't explicitly listed.

Cause 1 — the header simply isn't in the allow-list yet

// client sends a custom header:
fetch("https://api.example.com/data", {
  headers: { "X-API-Key": "abc123" },
});

// ❌ server's preflight response only allows the default/standard set:
res.set("Access-Control-Allow-Headers", "Content-Type");

// ✅ explicitly add every custom header your client actually sends:
res.set("Access-Control-Allow-Headers", "Content-Type, X-API-Key, Authorization");

This is the straightforward, most common case: a new header was added to a client-side request (often during feature work — adding an API key, a client-version header, a trace/correlation ID) without a corresponding update to the server's CORS configuration. Because Access-Control-Allow-Headers is an explicit allow-list, not a deny-list, any header not named there is rejected by default — there's no way to "allow everything except a small blocked set," so every custom header addition on the client requires a matching addition on the server.

Cause 2 — CORS middleware configured once, then a new header added later without updating it

// Express cors() middleware, configured months ago:
app.use(cors({
  origin: "https://app.example.com",
  allowedHeaders: ["Content-Type", "Authorization"],
}));

// a new feature later adds this client-side, without anyone
// remembering to touch the CORS config above:
fetch(url, { headers: { "X-Client-Version": "2.4.0" } });
// → X-Client-Version is not allowed by Access-Control-Allow-Headers

Explicit allowedHeaders configuration is a common source of drift over time: the list is usually set up once, early in a project, matching whatever headers existed at that point — and every subsequent feature that introduces a new header (a feature flag header, an experiment/AB-test header, a client-version or trace-ID header) silently breaks cross-origin requests unless whoever adds the client-side header remembers to also update this separate, easy-to-forget server-side list. Some teams standardize on a wildcard (allowedHeaders: '*' or reflecting the requested headers back from Access-Control-Request-Headers) specifically to eliminate this maintenance burden for non-credentialed APIs.

Cause 3 — wildcard used, but the request is credentialed

// client sends credentials alongside a custom header:
fetch(url, {
  credentials: "include",
  headers: { "Authorization": "Bearer " + token },
});

// ❌ wildcard doesn't apply to credentialed requests, same rule as
//    Access-Control-Allow-Origin:
res.set("Access-Control-Allow-Headers", "*");

// ✅ name the header explicitly instead:
res.set("Access-Control-Allow-Headers", "Authorization, Content-Type");
res.set("Access-Control-Allow-Credentials", "true");

The same credential-related restriction that governs Access-Control-Allow-Origin applies to headers too: a wildcard Access-Control-Allow-Headers: * is not honored once the request carries credentials, and Authorization specifically is never covered by a wildcard under any circumstances — it must always be listed by its literal name. This is a deliberate, security-motivated exception in the specification: the header most likely to carry sensitive authentication material is the one the spec refuses to let a blanket wildcard silently cover, forcing an explicit, auditable allow-list entry instead.

Cause 4 — the header is added conditionally, breaking only some requests

// this request stays "simple" — no preflight, always works:
fetch("/api/public-data");

// this one only preflights (and only needs an allow-list entry)
// once a user is actually logged in:
function apiCall(url) {
  const headers = {};
  if (currentUser) headers["Authorization"] = `Bearer ${currentUser.token}`;
  return fetch(url, { headers });
}

Because "simple requests" (no custom headers, standard methods, restricted content types) skip preflight entirely, a codebase that conditionally attaches a header based on application state can pass all of its manual/anonymous testing while failing in production for logged-in users — the developer's test session simply never exercised the code path that adds the header requiring an allow-list entry. This is a common gap between local testing and production behavior specifically because the two code paths (authenticated vs. anonymous) trigger genuinely different CORS enforcement rules, not just different application logic.

Common variants

Header named in the errorTypical source
authorizationbearer token/JWT auth added after initial CORS setup
x-requested-withadded automatically by some HTTP client libraries (older jQuery/Axios defaults)
content-type (with a JSON value)application/json is not in the CORS-safelisted Content-Type set
a custom x-* headerapp-specific: API keys, trace IDs, feature flags, client version

Debugging checklist

Frequently Asked Questions

What does "request header field is not allowed by Access-Control-Allow-Headers" mean?

Your JavaScript is sending a custom or non-simple header (e.g. Authorization, X-API-Key, X-Requested-With) on a cross-origin request, and the server's preflight OPTIONS response didn't list that specific header name in its Access-Control-Allow-Headers response header. The browser checks the requested header against this allow-list and blocks the entire request if even one header isn't explicitly permitted.

Why does adding a header in my fetch() call trigger a whole new class of error?

Any header outside a small "CORS-safelisted" set (Accept, Accept-Language, Content-Language, and Content-Type restricted to three specific values) automatically upgrades a request from "simple" to one that requires a preflight OPTIONS check first. Adding an Authorization header, a custom X-* header, or a JSON Content-Type is exactly what pushes a request into this category — it's not a bug, it's the browser correctly enforcing the CORS spec's default-deny stance on anything non-standard.

Is Access-Control-Allow-Headers case-sensitive?

Per the Fetch/CORS specification, header name matching for this purpose is meant to be case-insensitive, and modern browsers implement it that way — but relying on that isn't good practice. If you're debugging an inconsistency across browsers or a proxy that might be case-sensitive itself, normalize to the conventional casing (e.g. Authorization, X-Requested-With) on both the client-sent header and the server's allow-list to eliminate the variable entirely.

Can I just set Access-Control-Allow-Headers: * to avoid listing every header?

You can, and it works for non-credentialed requests, but the same restriction that applies to Access-Control-Allow-Origin applies here: the wildcard is not honored for Authorization specifically (it must always be explicitly named even under a wildcard policy) nor for any header on a credentialed request. For anything involving cookies or bearer tokens, list the actual header names explicitly rather than relying on the wildcard.

Why does this fail only for some users and not others on the same app?

This is usually caused by a code path that conditionally adds a header — for example, only attaching an Authorization header once a user is logged in, or only setting a custom X-Client-Version header after a recent client update. Requests that don't hit that code path stay "simple" and never preflight, while requests that do hit it require an allow-list entry the server doesn't have yet.

Does the server actually need to read the header, or just allow-list it?

The CORS allow-list check happens purely at the preflight/browser-enforcement layer and is entirely independent of whether your server-side code ever reads or uses that header's value. You must list a header in Access-Control-Allow-Headers if the client sends it, even if the server-side handler ignores it completely — the browser doesn't know or care what your handler does with it.

More CORS & network errors

Browse the full reference for CORS, network, and browser security errors — exact message, cause, and fix.

All Error References CORS: Preflight access control check HTTP Status Codes
About the author

Pasindu Ishan is a software developer based in Sri Lanka. He builds privacy-first developer tools at JSON Dev Tools.