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 preflightContent-Type: application/jsonX-Api-KeyAuthorization: Bearer xContent-Type: text/plain
(absent)blockedblockedblockedOK, no preflight
content-typeOKblockedblockedOK, no preflight
Content-TypeOKblockedblockedOK, no preflight
x-api-keyblockedOKblockedOK, no preflight
*OKOKOKOK, no preflight
* + credentials: 'include', exact ACAO, ACAC trueblockedblockedblockedOK, no preflight
authorizationblockedblockedOKOK, no preflight
*, authorizationOKOKOKOK, 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

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.