Access-Control-Allow-Headers (tested)
Access-Control-Allow-Headers goes on the response to a CORS preflight and lists the request headers the real request may carry. We tried eight header values against four cross-origin POSTs and logged what reached the server.
1. The results
Page at http://localhost:8973, cross-origin fetch() POST to http://127.0.0.1:8974. Every response carried Access-Control-Allow-Origin: * (or the exact origin plus Access-Control-Allow-Credentials: true in the credentials row). "OK" means OPTIONS then POST reached the server and fetch resolved; "blocked" means only the OPTIONS preflight arrived and fetch threw TypeError: Failed to fetch.
| Access-Control-Allow-Headers on preflight | Content-Type: application/json | X-Api-Key | Authorization: Bearer x | Content-Type: text/plain |
|---|---|---|---|---|
| (absent) | blocked | blocked | blocked | OK, no preflight |
content-type | OK | blocked | blocked | OK, no preflight |
Content-Type | OK | blocked | blocked | OK, no preflight |
x-api-key | blocked | OK | blocked | OK, no preflight |
* | OK | OK | OK | OK, no preflight |
* + credentials: 'include', exact ACAO, ACAC true | blocked | blocked | blocked | OK, no preflight |
authorization | blocked | blocked | OK | OK, no preflight |
*, authorization | OK | OK | OK | OK, no preflight |
The preflight's Access-Control-Request-Headers arrived lowercase (content-type, x-api-key, authorization). Each blocked request produced this console error:
Access to fetch at 'http://127.0.0.1:8974/x1' from origin 'http://localhost:8973' has been blocked by CORS policy: Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response.
2. What this means
- JSON needs it.
Content-Type: application/jsonis not a safelisted value, so it triggers a preflight and must be listed.text/plainis safelisted and never sent a preflight. - Header names are case-insensitive.
content-typeandContent-Typebehaved identically. - Only listed headers pass. Allowing
x-api-keydid not allow Content-Type or Authorization; list every non-safelisted header the request sends. *is a wildcard only without credentials. Withcredentials: 'include',*was treated literally and every preflighted request was blocked. In this Chromium build, plain*without credentials also letAuthorizationthrough; the Fetch spec says the wildcard does not cover Authorization, so*, Authorizationis the portable form.
3. Answering OPTIONS in nginx (not executed)
This snippet is doc-based; we did not run it.
location /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, PATCH, DELETE" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Api-Key" always;
add_header Access-Control-Max-Age 600 always;
add_header Vary Origin always;
return 204;
}
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Vary Origin always;
proxy_pass http://backend;
}
Related: Access-Control-Allow-Methods, Access-Control-Max-Age (preflight caching) and when the browser sends the Origin header.
Frequently asked questions
Why does my JSON POST fail CORS?
Content-Type: application/json triggers a preflight. In our Chromium 153 test it was blocked with 'Request header field content-type is not allowed by Access-Control-Allow-Headers in preflight response' until the preflight answered Access-Control-Allow-Headers: Content-Type.
Is Access-Control-Allow-Headers case-sensitive?
No. content-type and Content-Type both allowed a JSON POST in our test.
Does Access-Control-Allow-Headers: * work with credentials?
No. With credentials: 'include' plus an exact origin and Allow-Credentials: true, * was treated literally and JSON, X-Api-Key and Authorization requests were all blocked. List the headers explicitly.
Does * cover the Authorization header?
In Chromium 153 without credentials it did. The Fetch spec excludes Authorization from the wildcard, so send '*, Authorization' (which also worked) or list it explicitly.
Written with AI assistance. Measured on 2026-10-01 in full Chromium 153.0.8010.12 (Playwright, channel 'chromium', fresh browser context per row). The page was served from http://localhost:8973 and a Node.js API on http://127.0.0.1:8974 (a different origin) logged every request, OPTIONS preflights included with their Access-Control-Request-Headers, and answered preflights with 204 plus the Access-Control-Allow-Headers value under test. The nginx snippet is doc-based, not executed. PlainHeaders is our extension; the other methods are free.