HTTP

Header

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.

5 min read intermediate Try in Playground

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

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

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

## Syntax

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

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

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

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

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

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

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

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

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' })
})
```http

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

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

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

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

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

### 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)
})

Best Practices

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

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

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

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

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

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

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

### 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 (600 seconds in Firefox, 7200 seconds in Chrome), 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.

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.

Keep Learning