A browser says your API request failed, but the same endpoint works in Postman. You check the URL, payload, and token. Everything looks right. Then you notice this in the browser console:
Request header field x-client-version is not allowed by
Access-Control-Allow-Headers in preflight response.
The key is preflight. Before sending some requests to another origin, the browser first sends an OPTIONS request to ask which methods and headers the server allows. If that check fails, the browser can stop before your actual POST or PATCH reaches the route. Your POST or PATCH may never have reached the application route.
Here we focus on one common failure: the browser asks permission to send headers such as authorization or content-type, but the server replies with an incomplete Access-Control-Allow-Headers list. Compare those two lists before changing the request body or token. Once you compare those two lists, the error becomes much easier to diagnose.
Read the browser’s OPTIONS request first
Suppose a page on https://app.example.com needs to update an order through https://api.example.com. The application intends to send something like this:
POST /api/orders HTTP/1.1
Origin: https://app.example.com
Authorization: Bearer <token>
Content-Type: application/json
X-Client-Version: 2026.10
Because this is a cross-origin browser request with headers that require permission, the browser first sends an OPTIONS request. The crucial part looks like this:
OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization,content-type,x-client-version
Read the last line as a question: If I send the real POST, may it include these three headers?
Now imagine the API responds:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Authorization, Content-Type
This looks almost correct. It allows the origin and method, and it mentions two of the three headers. But x-client-version is missing. The browser rejects the preflight instead of silently dropping that header and continuing.
For this request, the response needs to allow all three:
Access-Control-Allow-Headers: Authorization, Content-Type, X-Client-Version
That’s the central comparison throughout this article: Access-Control-Request-Headers is the browser’s question; Access-Control-Allow-Headers is the server’s answer.
Start in DevTools, not by changing fetch() again
When you see this error, open the browser’s Network panel, reproduce the request, and look for OPTIONS on the API endpoint. You may need to filter by method or enable the display of preflight requests, depending on your browser.
On that OPTIONS request, inspect:
-
Request headers: Origin, Access-Control-Request-Method, and Access-Control-Request-Headers.
-
Response status: whether the server returned a successful status or an error before CORS handling.
-
Response headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, and especially Access-Control-Allow-Headers.
Write down the requested names and the allowed names. Don’t worry if the casing differs: HTTP field names are compared case-insensitively. x-client-version and X-CLIENT-VERSION refer to the same header name.
One useful detail: the browser assembles Access-Control-Request-Headers itself. It commonly displays names in lowercase, even if your JavaScript used title casing. You shouldn’t try to set that preflight header in frontend code.
The JavaScript error alone rarely gives you the full exchange. The Network panel lets you find out whether you have a missing header, a rejected method, a wrong origin, or a request that never reached the CORS middleware.
Why even Content-Type may trigger the header check
You may be surprised to see content-type in the browser’s requested list. Isn’t Content-Type an ordinary HTTP header?
Yes, but the CORS safelist is narrower than “ordinary HTTP headers.” For Content-Type, the safelisted media types are application/x-www-form-urlencoded, multipart/form-data, and text/plain, subject to other restrictions. application/json isn’t one of them.
So even a small JSON request can cause the browser to preflight:
await fetch("https://api.example.com/profile", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Sneha" }),
});
In this case, content-type can appear in Access-Control-Request-Headers. If the server doesn’t permit it, the real POST won’t follow.
Custom headers are another common trigger. Names such as X-Request-ID, X-Tenant-ID, X-API-Key, and X-Client-Version aren’t CORS-safelisted request headers. If a frontend library starts adding one automatically, a previously working request may begin failing at preflight even though the endpoint hasn’t changed.
The sensible fix depends on the API contract. If the header is required, allow it in the server’s CORS policy. If it serves no purpose, remove it from the actual client request. Don’t add headers just to make the error message go away.
One missing name is enough to stop the request
Consider this example:
await fetch("https://api.example.com/profile", {
method: "PATCH",
headers: {
Authorization: "Bearer <token>",
"Content-Type": "application/json",
"X-Request-ID": "req_123",
},
body: JSON.stringify({ name: "Sneha" }),
});
The browser might ask to send authorization,content-type,x-request-id. If the preflight response permits only Authorization, Content-Type, the browser doesn’t send a reduced PATCH without X-Request-ID. It blocks the request.
This also explains a frustrating debugging pattern: you allow the first header named in the error, refresh, and get a complaint about another header. Rather than fixing them one at a time, compare the entire requested list with the allowed policy. Check all relevant headers, as well as the method and origin, in a single pass.
Three fixes that look right but aren’t
Sending Access-Control-Allow-Headers from JavaScript
This is a server response policy header, not a way for the client to grant itself permission:
// Incorrect: this does not authorize your request.
fetch(url, {
headers: { "Access-Control-Allow-Headers": "*" },
});
In fact, this adds another request header. The browser may then complain that access-control-allow-headers itself isn’t permitted. Put CORS policy on the server, API gateway, proxy, or edge layer that responds to OPTIONS.
Changing Access-Control-Expose-Headers
These two similarly named response headers do different jobs:
| Header | What it controls |
|---|---|
| Access-Control-Allow-Headers | Which request headers the browser may send after a preflight |
| Access-Control-Expose-Headers | Which response headers frontend JavaScript may read |
If the browser isn’t allowed to send X-Request-ID, changing Access-Control-Expose-Headers won’t fix it. That header matters later, when code tries to read a response header.
Setting mode: “no-cors”
fetch(url, { mode: "no-cors" });
This doesn’t give JavaScript unrestricted access to the API. It limits the request and produces an opaque response that your code can’t normally inspect. If you need to read an API response, repair the server’s CORS configuration rather than hiding the error with no-cors.
Why wildcard permission isn’t always permission
A tempting shortcut is:
Access-Control-Allow-Headers: *
The * here has conditions. Under the Fetch rules, it can match ordinary requested header names when the request’s credentials mode is not include. With credentials mode include, the asterisk is treated as a literal header name, not as an “allow everything” wildcard.
There’s also a special case that matters to many APIs: Authorization is a non-wildcard request-header name. It must be allowed explicitly, even when the request’s credentials mode isn’t include.
This distinction is easy to miss. A JavaScript Authorization header and the Fetch option credentials: "include" are related to authentication, but they aren’t the same switch. Don’t assume one is set merely because the other exists.
For an API that expects these headers, an explicit allow-list is clearer:
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-ID
If cookies or other credentials are being used cross-origin, you also have to satisfy the separate origin-and-credentials CORS requirements. In particular, a credentialed response can’t use Access-Control-Allow-Origin: *. Even when header permission is fixed, the request can still fail a different CORS check.
Six cases that reveal the matching rules
The frozen HTTP-1 research checked six combinations against the relevant Fetch header-matching rules. This table is a protocol-rule illustration, not a claim that every browser and framework was tested end to end.
| Requested header names | Server allows | Credentials mode | Expected result |
|---|---|---|---|
| x-client-version | Content-Type | omit | Fail — name missing |
| content-type, x-client-version | Content-Type, X-CLIENT-VERSION | omit | Pass — case doesn’t matter |
| x-client-version | * | omit | Pass — ordinary name |
| x-client-version | * | include | Fail — wildcard is literal |
| authorization | * | omit | Fail — Authorization needs explicit permission |
| authorization | Authorization | omit | Pass — explicitly allowed |
The takeaway isn’t to memorize six rows. It’s to check which headers were requested, whether the allow-list covers them, and whether either of the wildcard exceptions applies. That’s faster than experimenting with random combinations of asterisks and header names.
If OPTIONS returns 401, 403, 404, or 405
Sometimes the console points toward a header problem, but DevTools shows an error status for the OPTIONS request. That changes your investigation.
Preflight requests are protocol probes. They don’t carry the application’s normal credentials in the way its actual authenticated request may. If authentication middleware rejects OPTIONS before the CORS handler can respond, the server may return 401 with none of the expected allow headers. A router that doesn’t accept OPTIONS may return 404 or 405 instead.
Don’t keep adjusting the allow-list if the preflight isn’t reaching it. Trace the order of your routes, authentication middleware, gateway rules, and edge handlers. An acceptable CORS preflight requires a successful HTTP status along with the relevant permissions.
It’s also possible to fix the header-name mismatch and still fail because Access-Control-Allow-Origin is wrong or the requested method isn’t allowed. Treat origin, method, headers, and credential checks as separate parts of the same handshake.
Diagnose the header mismatch in this order
When you next see the error, use this short sequence:
-
Reproduce it with DevTools open. Find the OPTIONS request, not just the failed JavaScript promise.
-
Read the browser’s request. Note Origin, Access-Control-Request-Method, and every name in Access-Control-Request-Headers.
-
Check the response status. If OPTIONS is rejected, inspect routing and middleware before changing CORS header values.
-
Compare the policy. Check Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. Remember case-insensitive names and the wildcard rules.
-
Fix the layer sending the response. That might be application middleware, an API gateway, a reverse proxy, or a serverless edge function.
-
Retest the preflight, then the real request. Passing OPTIONS only means the browser can proceed; the endpoint may still return its own application error.
Here’s a sensible example for an API expecting a JSON POST with a bearer token and an application version header:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Authorization, Content-Type, X-Client-Version
Vary: Origin
Those values are illustrative, not a configuration to paste onto every server. Permit only the origins, methods, and headers your actual application needs. Avoid a policy that blindly reflects every requested header simply to make preflight pass.
Two small details can save time. First, tools such as cURL and Postman don’t enforce browser CORS, so a working POST there doesn’t establish that the browser’s preflight is configured correctly. You can use cURL to inspect an OPTIONS response, but it won’t enforce the browser’s decision for you. Second, a server may send Access-Control-Max-Age to cache successful preflight permissions. If you’re changing policy during development, make sure you’re observing a fresh exchange before concluding that the change didn’t work.
What the formatter can help with—and what it can’t
Yoryantra’s HTTP Request Formatter helps you separate the method, URL, headers, and body of the actual request you’re trying to send. That’s useful when the original request comes from a log, proxy trace, or code sample and you need to see exactly what changed.
But formatting a request isn’t a CORS test. The browser and server still have to negotiate the preflight. Use the formatter to understand your intended request; use DevTools to understand what the browser asked permission to send and what the server actually allowed.
The practical rule is simple: when the error names Access-Control-Allow-Headers, compare the request-header list in the OPTIONS request with the server’s response before touching unrelated frontend code. In many cases, that one comparison points directly to the missing configuration.
