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

Source: https://howhttpworks.com/headers/access-control-request-headers
Last reviewed: 2026-10-04

> **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:**

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

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

```http
Access-Control-Request-Headers: <header-name>
Access-Control-Request-Headers: <header-name>, <header-name>, ...
```

Multiple headers are comma-separated.

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

- `Accept`
- `Accept-Language`
- `Content-Language`
- `Content-Type` (only for: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`)

Any other headers trigger a preflight.

## Common Examples

### API with Authentication

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

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

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

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

```javascript
// 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:**

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

```javascript
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:**

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

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

```javascript
// 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

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

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

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

```javascript
// ❌ 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)

```javascript
// ✅ 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

```javascript
// 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

```javascript
// ✅ 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

```text
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:**

```javascript
// Add the missing header to Access-Control-Allow-Headers
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Custom-Header')
```

### Error: Case Sensitivity

```javascript
// ❌ 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

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

```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](https://howhttpworks.com/headers/access-control-allow-headers) - Server response with allowed headers
- [Access-Control-Request-Method](https://howhttpworks.com/headers/access-control-request-method) - Preflight method request
- [Access-Control-Allow-Origin](https://howhttpworks.com/headers/access-control-allow-origin) - Allowed origins
- [Access-Control-Allow-Methods](https://howhttpworks.com/headers/access-control-allow-methods) - Allowed HTTP methods
- [Access-Control-Max-Age](https://howhttpworks.com/headers/access-control-max-age) - Preflight cache duration
- [Origin](https://howhttpworks.com/headers/origin) - Request origin
