# Why Adding CORS Headers to an OPTIONS Route Doesn't Always Enable CORS in Express

> Discover why adding CORS headers to your Express OPTIONS route may not enable CORS. Learn how Expresss internal router can prevent your headers from being applied.

- Repository: [expressjs/express](https://github.com/expressjs/express)
- Tags: deep-dive
- Published: 2026-02-21

---

**Adding CORS headers only to a custom `OPTIONS` route often fails because Express's internal router handles pre-flight requests first, terminating the response before your middleware can attach the necessary headers.**

When you attempt to enable CORS in Express by manually adding headers to an `OPTIONS` route, you may find browsers still blocking requests due to missing access controls. According to the expressjs/express source code, the framework's built-in router automatically generates `OPTIONS` responses for any registered path, often bypassing custom handlers entirely. Understanding this internal request lifecycle is essential for implementing reliable cross-origin resource sharing in your API.

## How Express Handles OPTIONS Requests Internally

Express processes incoming requests through a **middleware stack** that terminates at the internal **Router**. When a browser sends a CORS pre-flight request, it transmits an `OPTIONS` request to the target URL. The router contains built-in logic that automatically generates an `Allow` header from the routes matching that path (such as `GET, HEAD, PUT`) and **stops further processing** if no explicit `app.options()` handler exists for that specific path.

This default behavior is explicitly tested in the framework's test suite, where a plain `OPTIONS` request receives only the `Allow` header without any CORS-specific headers:

```javascript
request(app).options('/users')
  .expect('Allow', 'GET, HEAD, PUT')
  .expect(200, 'GET, HEAD, PUT', done);

```

*Source: [[`test/app.options.js`](https://github.com/expressjs/express/blob/main/test/app.options.js), lines 14-18](https://github.com/expressjs/express/blob/master/test/app.options.js#L14-L18)*

Because the router's internal `OPTIONS` handler executes **before any custom middleware that adds CORS headers**, simply attaching `Access-Control-Allow-*` headers to a user-defined `OPTIONS` route provides no guarantee those headers will reach the browser.

## When CORS Headers Get Ignored

Three specific architectural scenarios prevent your manually added CORS headers from being sent to the client:

### No Explicit app.options() Handler Defined

If you define routes like `app.get('/users')` but omit `app.options('/users')`, Express automatically generates an `OPTIONS` response containing only the `Allow` header. The router terminates the response immediately after generating this header, leaving no opportunity for CORS middleware to execute.

### Middleware Ordering Conflicts

When `app.options()` is defined after other middleware that terminates responses (such as authentication checks calling `res.end()`), the custom handler never executes. The preceding middleware sends a response to the client before your `OPTIONS` route can attach CORS headers, causing the browser to block the pre-flight request.

### Sub-App and Router Mounting Issues

In applications using sub-apps or mounted routers, a parent application may apply CORS middleware globally. However, the child router's auto-generated `OPTIONS` response executes within its own context, potentially bypassing the parent's CORS middleware entirely. The pre-flight response returns to the browser lacking the required `Access-Control-Allow-Origin` header.

## The Solution: Apply CORS Globally

To reliably enable CORS in Express, you must attach headers on every request rather than solely on a manual `OPTIONS` route. The standard pattern uses the official **`cors` middleware**, which runs early in the middleware stack and automatically adds the required headers for both pre-flight (`OPTIONS`) and actual requests:

```javascript
const express = require('express')
const cors = require('cors')

const app = express()

// Apply CORS to all routes, before any other middleware
app.use(cors())          // <-- adds Access-Control-Allow-Origin, etc.
app.use(express.json()) // other middleware ...

app.get('/users', (req, res) => {
  res.json({ name: 'Alice' })
})

```

The `app.use()` implementation that makes this ordering possible resides in [[`lib/application.js`](https://github.com/expressjs/express/blob/main/lib/application.js)](https://github.com/expressjs/express/blob/master/lib/application.js#L90-L118), where middleware registration establishes the execution sequence that ensures `cors()` processes requests before the router's internal handling.

## Customizing Pre-Flight Responses

If you require a **custom pre-flight response** (for example, to restrict allowed methods to a subset), you can still use `app.options()` **in addition** to the global CORS middleware. The global middleware sets the standard CORS headers, while your specific handler modifies the `Allow` header:

```javascript
app.options('/users', (req, res) => {
  // cors() already set generic headers; add any custom ones here
  res.set('Allow', 'GET')
  res.sendStatus(204)   // No body required for pre-flight
})

```

The test suite confirms that `app.options()` overrides the default behavior when properly defined:

```javascript
app.options('/users', (req, res) => {
  res.set('Allow', 'GET')
  res.send('GET')
})

```

*Source: [[`test/app.options.js`](https://github.com/expressjs/express/blob/main/test/app.options.js), lines 99-115](https://github.com/expressjs/express/blob/master/test/app.options.js#L99-L115)*

## Summary

- **Express's built-in `OPTIONS` response does not include CORS headers** and terminates before custom middleware executes.
- Adding CORS headers **only** to a custom `OPTIONS` handler works **only if that handler is actually executed**, which the router's default handling can prevent.
- The safest approach to enable CORS in Express uses the **`cors` middleware early** in the stack (or a custom middleware running before the router) so every request—including automatic `OPTIONS` responses—receives the correct headers.
- When you need custom pre-flight logic, combine global CORS middleware with specific `app.options()` handlers to override default `Allow` headers.

## Frequently Asked Questions

### Why does my browser still block requests even though I added CORS headers to my OPTIONS route?

Express's internal router automatically handles `OPTIONS` requests when no explicit `app.options()` handler exists, generating an `Allow` header and terminating the response before your custom headers are attached. This default behavior in [`lib/application.js`](https://github.com/expressjs/express/blob/main/lib/application.js) and the internal router sends a response to the browser that lacks `Access-Control-Allow-Origin` and related headers, causing the security block.

### What is the correct order for CORS middleware in Express?

You must apply CORS middleware **before** any route handlers or other middleware that might terminate the request. According to the implementation in [`lib/application.js`](https://github.com/expressjs/express/blob/main/lib/application.js), middleware executes in the order registered via `app.use()`, so place `app.use(cors())` at the top of your middleware stack to ensure it runs before the router's internal `OPTIONS` handling.

### Can I use app.options() instead of the cors middleware package?

While you can use `app.options()` to manually set headers, relying on it exclusively is unreliable because it executes only if the router hasn't already auto-generated an `OPTIONS` response. The `cors` middleware package is the recommended approach because it intercepts all requests—including the automatic ones—ensuring consistent header application across your entire API surface.

### How does Express generate the automatic Allow header for OPTIONS requests?

When a request matches a path with registered methods (like `GET` or `POST`) but has no explicit `OPTIONS` handler, Express iterates through the route stack and compiles a list of supported HTTP methods. It then sends an `OPTIONS` response with the `Allow` header set to those methods (for example, `Allow: GET, HEAD, PUT`) and a 200 status, as demonstrated in [`test/app.options.js`](https://github.com/expressjs/express/blob/main/test/app.options.js).