# Access-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.

Source: https://howhttpworks.com/headers/access-control-allow-headers
Last reviewed: 2026-10-05

> **TL;DR:** If your API takes JSON and bearer tokens, return `Access-Control-Allow-Headers: Content-Type, Authorization` on the preflight response, or the browser won't send the real request. Header names match case-insensitively. A `*` wildcard never covers `Authorization`, and when the request's credentials mode is `include` it 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](https://howhttpworks.com/headers/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](https://howhttpworks.com/headers/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.

```http
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
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

```http
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:

```http
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

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Headers#directives).

For a file upload, let the browser write the multipart `Content-Type` and its boundary:

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects#gotchas).

## Server Implementation

For an existing Express app with the `cors` package installed:

```javascript
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](https://expressjs.com/en/resources/middleware/cors/).

FastAPI expresses the same policy with `CORSMiddleware`:

```python
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](https://fastapi.tiangolo.com/tutorial/cors/). 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:

```python
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](https://github.com/adamchainz/django-cors-headers#configuration) 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:

```javascript
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](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header).

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:

```http
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

```bash
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

- [Access-Control-Allow-Origin](https://howhttpworks.com/headers/access-control-allow-origin)
- [Access-Control-Allow-Methods](https://howhttpworks.com/headers/access-control-allow-methods)
- [Access-Control-Request-Headers](https://howhttpworks.com/headers/access-control-request-headers)
- [Access-Control-Expose-Headers](https://howhttpworks.com/headers/access-control-expose-headers)
