Response header
< Access-Control-Allow-Headers: Content-Type, AuthorizationAccess-Control-Allow-Headers Header
Learn how Access-Control-Allow-Headers specifies which custom HTTP headers can be used during cross-origin requests in CORS preflight responses.
- Direction
- Response
- Category
- CORS
- Spec
- Fetch Standard
On this page
TL;DR: If your API takes JSON and bearer tokens, return
Access-Control-Allow-Headers: Content-Type, Authorizationon the preflight response, or the browser won’t send the real request. Header names match case-insensitively. A*wildcard never coversAuthorization, and when the request’s credentials mode isincludeit isn’t a wildcard at all.
What is Access-Control-Allow-Headers?
It’s the server’s answer to the browser’s Access-Control-Request-Headers: “yes, you may send these request headers.” It’s about headers the client sends. Letting JavaScript read headers from the response is a separate job, handled by Access-Control-Expose-Headers.
How Access-Control-Allow-Headers Works
Here’s a preflight for a JSON POST. Headers on this page are trimmed examples.
OPTIONS /api/orders HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type, Authorization
If either requested name is missing from the answer, the browser stops and never sends the POST. When the preflight passes, the actual response still needs its own Access-Control-Allow-Origin.
Syntax
Access-Control-Allow-Headers: Content-Type, Authorization, X-Request-ID
List header names, comma-separated, in any case. Values such as application/json or Bearer ... never go here.
Common Examples
You need Content-Type for application/json bodies and Authorization when the client sends a bearer token. Add X-Request-ID only if the client really sends that header. If the server generates the request ID, it belongs on the response, and in Access-Control-Expose-Headers if JavaScript needs to read it.
A cookie-based app might send a CSRF token as X-CSRFToken, and an API-key service might use X-API-Key. Neither name is safelisted, so list whichever ones your endpoint accepts:
Access-Control-Allow-Headers: Content-Type, X-CSRFToken
Allowing a header name says nothing about its value. Your POST handler still has to check the CSRF token or API key. The header lists names only; the secret itself never goes in Access-Control-Allow-Headers.
Real-World Scenarios
await fetch('https://api.example.com/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer REPLACE_WITH_TOKEN'
},
body: JSON.stringify({ item: 'book' })
})
The browser builds the preflight on its own. JavaScript never sets Access-Control-Request-Headers. MDN documents the wildcard and Authorization rules.
For a file upload, let the browser write the multipart Content-Type and its boundary:
const form = new FormData()
form.append('document', document.querySelector('#upload').files[0])
await fetch('https://api.example.com/api/documents', {
method: 'POST',
headers: { Authorization: 'Bearer REPLACE_WITH_TOKEN' },
body: form
})
The page needs an <input type="file" id="upload">. Authorization still triggers a preflight here. Setting Content-Type: multipart/form-data yourself drops the generated boundary, and the server may fail to parse the body. MDN explains this FormData constraint.
Server Implementation
For an existing Express app with the cors package installed:
const cors = require('cors')
app.use('/api', cors({
origin: 'https://app.example.com',
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID']
}))
Mount this before your API routes and before any authentication that would reject an unauthenticated OPTIONS request. Application-level cors middleware handles preflights.
FastAPI expresses the same policy with CORSMiddleware:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_methods=["GET", "POST"],
allow_headers=["Content-Type", "Authorization", "X-Request-ID"],
)
@app.get("/api/status")
def status():
return {"status": "ok"}
FastAPI documents these options. The middleware only grants CORS permission, so you still register the API’s POST handler yourself.
For Django with django-cors-headers installed, add corsheaders to INSTALLED_APPS and put corsheaders.middleware.CorsMiddleware above django.middleware.common.CommonMiddleware. Then extend the package’s default header list in settings:
from corsheaders.defaults import default_headers
CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_URLS_REGEX = r"^/api/.*$"
CORS_ALLOW_HEADERS = (*default_headers, "x-request-id")
The package documentation covers the settings and middleware order. CORS and Django’s CSRF protection are separate: listing a CSRF header here doesn’t exempt the request from CSRF validation.
Getting Access-Control-Allow-Headers right
Browsers send requested names in lowercase. If you match them yourself against a case-sensitive array like ['Authorization'], nothing will match. Normalize both sides, or simply return a fixed list of permitted names.
Allowing Cookie won’t let frontend code set it. It’s a forbidden request header that scripts can’t touch. Whether cookies go along is decided by the credentials mode and cookie policy.
If your own handler inspects the requested list, compare normalized names:
const permitted = new Set(['content-type', 'authorization', 'x-request-id'])
const requested = (req.get('Access-Control-Request-Headers') || '')
.split(',').map((name) => name.trim().toLowerCase()).filter(Boolean)
const allPermitted = requested.every((name) => permitted.has(name))
Treat allPermitted as one part of preflight validation, alongside the origin and method checks. On success, return your configured list. Echoing back whatever names the browser asked for turns the allowlist into a no-op.
CORS Simple vs Preflighted Requests
The safelist covers Accept, Accept-Language, Content-Language, Content-Type with restricted values, and Range with a single byte range. Values matter as much as names: Content-Type: application/json is not safelisted. The only safelisted content types are application/x-www-form-urlencoded, multipart/form-data and text/plain, and the other value restrictions still apply. MDN lists those restrictions.
So Range: bytes=0-499 can qualify, while a multi-range request like bytes=0-99,200-299 doesn’t. You can’t tell whether a request needs a preflight from header names alone.
Common Header Combinations
For a cookie-authenticated JSON endpoint, the preflight response looks like this:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Methods: POST
When credentials mode is include, list header names explicitly, even if no cookie happens to exist yet. Authorization is the one name the wildcard never covers. There’s no wider category of “sensitive headers” that * skips.
Testing Access-Control-Allow-Headers
curl -i -X OPTIONS https://api.example.com/api/orders \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: authorization,content-type'
curl shows you the response but doesn’t enforce anything, so compare the requested names with the returned list in the browser’s Network panel. If the server logs show only an OPTIONS request, the preflight failed; fix that before you touch the POST handler.
A classic failure: a shared fetch wrapper adds X-Request-ID to every call, but the server only allows Content-Type. Read the list the browser actually requested. A hand-written curl command with fewer names will pass and miss the problem. And if a proxy answers OPTIONS before your app sees it, changing the app’s middleware won’t change that response.
Related Headers
Frequently asked questions
What is Access-Control-Allow-Headers?
This CORS response header permits request header names during preflight. Authorization and other non-safelisted request headers require permission before the browser sends the actual request.
Which headers need to be allowed?
CORS-safelisted request headers do not need explicit permission when their values satisfy the safelist restrictions. JSON Content-Type, Authorization, and custom headers need permission.
Why is my Authorization header blocked?
Authorization is a CORS non-wildcard request header name and must be listed explicitly. A wildcard never grants permission for it.
Can I use wildcard for headers?
The wildcard covers non-safelisted names other than Authorization only when the request credentials mode is not include. With include, it is treated as the literal header name *.
Sources
Related
Access-Control-Expose-Headers: Read Headers in fetch()
Fix response.headers.get() returning null on cross-origin fetch. Access-Control-Expose-Headers, safelisted headers, and why * fails with credentials.
Access-Control-Allow-Credentials Header
Learn how Access-Control-Allow-Credentials controls whether browsers expose responses when credentials (cookies, auth headers) are included in CORS requests.
Access-Control-Allow-Methods Header
Learn how Access-Control-Allow-Methods specifies which HTTP methods are permitted for cross-origin requests in CORS preflight responses.
Access-Control-Allow-Origin Header: CORS Errors and Fixes
Access-Control-Allow-Origin explained: exact browser errors, why * fails with credentials, why you need Vary: Origin, and fixes for nginx, Express and Django.