Request header
> Access-Control-Request-Headers: content-type, authorizationAccess-Control-Request-Headers Header
Learn how Access-Control-Request-Headers tells servers which custom headers will be used in CORS requests. Essential for preflight request handling.
- Direction
- Request
- Category
- CORS
- Spec
- Fetch Standard
- JS can set it
- No: forbidden header name
On this page
- What is Access-Control-Request-Headers?
- How Access-Control-Request-Headers Works
- Syntax
- When Preflight is Triggered
- Common Examples
- API with Authentication
- JSON API with Custom Headers
- Multiple Authentication Methods
- GraphQL Request
- Real-World Scenarios
- React Application with JWT Authentication
- API with Request Tracking
- Microservices with Service Mesh Headers
- Server-Side Handling
- Express.js Middleware
- Manual Implementation
- Dynamic Header Validation
- Getting Access-Control-Request-Headers right
- 1. Specify Exact Headers Needed
- 2. Echo Requested Headers (With Validation)
- 3. Document Required Headers
- 4. Consistent Header Naming
- Common Errors and Solutions
- Error: Header Not Allowed
- Error: Case Sensitivity
- Testing
- Using curl
- Using JavaScript
- Preflight Caching and Performance
- Related Headers
TL;DR: Sent in CORS preflight requests to ask permission for specific headers. Server must allow these headers for the actual request to proceed.
What is Access-Control-Request-Headers?
The Access-Control-Request-Headers header is sent by browsers during a CORS preflight request to inform the server which HTTP headers the actual request will include. It’s like asking permission: “I want to send these custom headers - is that okay?”
This header only appears in OPTIONS preflight requests, not in the actual cross-origin requests themselves.
How Access-Control-Request-Headers Works
Browser sends preflight request:
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization, X-Custom-Header
Server responds with allowed headers:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header
Access-Control-Max-Age: 3600
Browser then sends the actual request:
POST /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGc...
X-Custom-Header: custom-value
{"name": "John Doe"}
Syntax
Access-Control-Request-Headers: <header-name>
Access-Control-Request-Headers: <header-name>, <header-name>, ...
Multiple headers are comma-separated.
# Single header
Access-Control-Request-Headers: Content-Type
# Multiple headers
Access-Control-Request-Headers: Content-Type, Authorization
# Many headers
Access-Control-Request-Headers: Content-Type, Authorization, X-API-Key, X-Request-ID
When Preflight is Triggered
Browsers automatically send preflight requests when you use non-simple headers. Simple headers (that don’t trigger preflight) include:
AcceptAccept-LanguageContent-LanguageContent-Type(only for:application/x-www-form-urlencoded,multipart/form-data,text/plain)
Any other headers trigger a preflight.
Common Examples
API with Authentication
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: Authorization
JSON API with Custom Headers
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, X-Request-ID
Multiple Authentication Methods
OPTIONS /api/secure HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Authorization, X-API-Key, Content-Type
GraphQL Request
OPTIONS /graphql HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, X-Request-ID, X-Client-Version
Real-World Scenarios
React Application with JWT Authentication
Client code:
// This triggers a preflight because of Authorization header
fetch('https://api.example.com/users', {
method: 'GET',
headers: {
Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
'Content-Type': 'application/json'
}
})
Automatic preflight request:
OPTIONS /users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: Authorization, Content-Type
Server must respond:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 3600
API with Request Tracking
Client code:
fetch('https://api.example.com/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Request-ID': 'abc-123',
'X-User-ID': 'user-456',
'X-Client-Version': '2.1.0'
},
body: JSON.stringify({ event: 'page_view' })
})
Preflight request:
OPTIONS /events HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, X-Request-ID, X-User-ID, X-Client-Version
Server response:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, X-Request-ID, X-User-ID, X-Client-Version
Access-Control-Max-Age: 7200
Microservices with Service Mesh Headers
Preflight request:
OPTIONS /api/orders HTTP/1.1
Host: orders.example.com
Origin: https://shop.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization, X-Trace-ID, X-Span-ID, X-User-Context
Server configuration:
// Node.js/Express
app.options('/api/*', (req, res) => {
const allowedHeaders = [
'Content-Type',
'Authorization',
'X-Trace-ID',
'X-Span-ID',
'X-User-Context',
'X-Request-ID'
]
res.setHeader('Access-Control-Allow-Origin', 'https://shop.example.com')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.setHeader('Access-Control-Allow-Headers', allowedHeaders.join(', '))
res.setHeader('Access-Control-Max-Age', '3600')
res.sendStatus(204)
})
Server-Side Handling
Express.js Middleware
const cors = require('cors')
app.use(
cors({
origin: 'https://app.example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
maxAge: 3600
})
)
Manual Implementation
app.options('/api/*', (req, res) => {
const requestedHeaders = req.headers['access-control-request-headers']
const allowedHeaders = ['Content-Type', 'Authorization', 'X-API-Key', 'X-Request-ID']
// Validate requested headers
if (requestedHeaders) {
const requested = requestedHeaders.split(',').map((h) => h.trim())
const allAllowed = requested.every(
(h) => allowedHeaders.includes(h) || allowedHeaders.includes(h.toLowerCase())
)
if (allAllowed) {
res.setHeader('Access-Control-Allow-Headers', requestedHeaders)
} else {
// Only allow the intersection
const intersection = requested.filter((h) => allowedHeaders.includes(h))
res.setHeader('Access-Control-Allow-Headers', intersection.join(', '))
}
}
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.setHeader('Access-Control-Max-Age', '3600')
res.sendStatus(204)
})
Dynamic Header Validation
app.options('/api/*', (req, res) => {
const requestedHeaders = req.headers['access-control-request-headers']
// Base headers always allowed
const baseHeaders = ['Content-Type', 'Accept']
// Conditional headers based on endpoint
const conditionalHeaders = []
if (req.path.startsWith('/api/auth')) {
conditionalHeaders.push('Authorization')
}
if (req.path.startsWith('/api/tracking')) {
conditionalHeaders.push('X-Request-ID', 'X-User-ID')
}
const allowedHeaders = [...baseHeaders, ...conditionalHeaders]
res.setHeader('Access-Control-Allow-Origin', req.headers.origin)
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE')
res.setHeader('Access-Control-Allow-Headers', allowedHeaders.join(', '))
res.setHeader('Access-Control-Max-Age', '3600')
res.sendStatus(204)
})
Getting Access-Control-Request-Headers right
1. Specify Exact Headers Needed
// ❌ Too permissive
res.setHeader('Access-Control-Allow-Headers', '*')
// ✅ Specific headers only
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
2. Echo Requested Headers (With Validation)
// ✅ Validate and echo
const requestedHeaders = req.headers['access-control-request-headers']
const allowedHeaders = ['Content-Type', 'Authorization', 'X-API-Key']
if (requestedHeaders) {
const requested = requestedHeaders.split(',').map((h) => h.trim().toLowerCase())
const allowed = allowedHeaders.map((h) => h.toLowerCase())
const validated = requested.filter((h) => allowed.includes(h))
if (validated.length > 0) {
res.setHeader('Access-Control-Allow-Headers', validated.join(', '))
}
}
3. Document Required Headers
// API documentation
/**
* POST /api/users
*
* Required Headers:
* - Content-Type: application/json
* - Authorization: Bearer <token>
*
* Optional Headers:
* - X-Request-ID: Request tracking ID
* - X-Client-Version: Client version number
*/
app.post('/api/users', authenticateToken, (req, res) => {
// Handler
})
4. Consistent Header Naming
// ✅ Consistent custom header prefix
const customHeaders = ['X-Request-ID', 'X-Client-Version', 'X-User-Context', 'X-Trace-ID']
// ❌ Inconsistent naming
const badHeaders = [
'RequestID', // Missing prefix
'X-client-version', // Inconsistent casing
'user_context', // Different format
'trace-id' // Missing prefix
]
Common Errors and Solutions
Error: Header Not Allowed
Access to fetch at 'https://api.example.com' from origin 'https://app.example.com'
has been blocked by CORS policy: Request header field X-Custom-Header is not
allowed by Access-Control-Allow-Headers in preflight response.
Solution:
// Add the missing header to Access-Control-Allow-Headers
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Custom-Header')
Error: Case Sensitivity
// ❌ Case mismatch can cause issues
Client sends: Access-Control-Request-Headers: content-type, authorization
Server allows: Access-Control-Allow-Headers: Content-Type, Authorization
// ✅ Normalize case
const normalizeHeaders = (headers) => headers.toLowerCase()
Testing
Using curl
# Test preflight with custom headers
curl -X OPTIONS https://api.example.com/users \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization, X-API-Key" \
-v
# Look for response header:
# Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key
Using JavaScript
// Browser automatically sends Access-Control-Request-Headers
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer token',
'X-API-Key': 'key123'
},
body: JSON.stringify({ name: 'Test' })
})
// Check Network tab for OPTIONS request
// Should see: Access-Control-Request-Headers: content-type, authorization, x-api-key
Preflight Caching and Performance
Every preflight request adds a round trip before the actual request, which can significantly impact performance for APIs that are called frequently. The Access-Control-Max-Age response header tells browsers how long to cache the preflight result, avoiding repeated OPTIONS requests for the same origin, method, and headers combination.
Set Access-Control-Max-Age to a high value (86400 seconds = 24 hours) for stable APIs where the allowed headers and methods rarely change. Browsers cap this value at their own maximum (86400 seconds in Firefox, 7200 in Chromium, 600 in Safari), so setting it higher than the browser maximum has no additional effect. For APIs under active development where you frequently add new allowed headers, a shorter cache time (300-600 seconds) reduces the risk of clients caching stale preflight results.
Related Headers
- Access-Control-Allow-Headers - Server response with allowed headers
- Access-Control-Request-Method - Preflight method request
- Access-Control-Allow-Origin - Allowed origins
- Access-Control-Allow-Methods - Allowed HTTP methods
- Access-Control-Max-Age - Preflight cache duration
- Origin - Request origin
Frequently asked questions
What is Access-Control-Request-Headers?
This header is sent in preflight requests listing custom headers the actual request will use. The server must allow these headers for the request to proceed.
When does the browser send this header?
When a cross-origin request includes custom headers like Authorization or X-Custom-Header, the browser sends a preflight with Access-Control-Request-Headers listing them.
How should servers respond?
Include the requested headers in Access-Control-Allow-Headers response. If any requested header is not allowed, the preflight fails and the actual request is blocked.
Which headers trigger preflight?
Any header not in the CORS-safelisted set triggers preflight. This includes Authorization, custom X- headers, and Content-Type with non-simple values like application/json.
Sources
Related
Access-Control-Request-Method Header
Learn how Access-Control-Request-Method tells servers which HTTP method will be used in the actual CORS request. Essential for preflight request handling.
Origin Header
Learn how the Origin header identifies where cross-origin requests come from. Essential for CORS security policies and preventing cross-site request forgery.
Sec-Fetch-Dest Header: All Values Explained
Sec-Fetch-Dest tells the server where a response will be used: document, iframe, image, script, empty. Full value list and server-side uses.
Sec-Fetch-Mode Header: Values and Server Use
Sec-Fetch-Mode reports the request mode: navigate, cors, no-cors, same-origin or websocket. What sets each value and how servers use it.