expressjs/cors

Node.js CORS middleware

6,195 stars JavaScript 7 components

7 hidden assumptions · 5-stage pipeline · 7 components

Like any codebase, this library makes assumptions it never checks — most are routine. The ones worth your attention are below, in plain language with what to do about each.

Adds CORS headers to HTTP responses so browsers allow cross-origin requests

An HTTP request arrives at an Express app that has cors() mounted. The cors() factory was already called at setup time with an options object or a dynamic resolver function; it returned a (req, res, next) closure that captured those options. When a request arrives, the closure either directly calls corsWithOptions() (static options) or invokes the user-supplied function with the request to get per-request options (dynamic path), then calls corsWithOptions(). corsWithOptions() calls six header-builder functions in sequence — configureOrigin, configureMethods, configureCredentials, configureMaxAge, configureAllowedHeaders, configureExposedHeaders — each returning one or two {key, value} HeaderDescriptors. These are collected into a flat array and passed to applyHeaders(), which writes the non-false values to the response via res.setHeader(). The vary() utility is also called directly on the response wherever a Vary header is needed (to safely append rather than overwrite). Finally, if the request is an OPTIONS preflight and preflightContinue is false, the response is ended immediately with the configured optionsSuccessStatus (default 204); otherwise next() is called and Express continues to the application's own route handlers.

Under the hood, the system uses 7 control points to manage its runtime behavior.

A 7-component library. 6 files analyzed. Data flows through 5 distinct pipeline stages.

Hidden Assumptions

Most of what this code assumes is routine. These 2 are the ones most likely to cause trouble here — in plain terms, with what to do about each. The rest are minor; they're under "Show everything".

Worth your attention first

If you turn on both 'allow any website' and 'allow cookies/login credentials' at the same time, browsers will block every single request — it's a rule browsers enforce strictly. The server will look like it's working fine in your logs, but users in the browser get a confusing access error. This is a very common misconfiguration and there is no warning anywhere in the tool to stop you.

What to do: If you need cookies or login sessions to work cross-origin, you must also specify exactly which website is allowed — not 'any website' — and the tool will not remind you of this, so double-check your settings before going live.

Worth a check

By default, if you don't tell the tool which specific request headers are allowed, it just says 'yes' to whatever any browser asks for. This means a page from another website could ask permission to send unusual or sensitive headers, and your server will approve it automatically without you realising. Most people never configure this setting and never notice.

What to do: Explicitly list the headers your app actually needs in the configuration rather than relying on the default mirror-everything behavior, especially if your app handles sensitive data.

Show everything (5 more)
Contract

Any request that uses the OPTIONS method — including ones from API testing tools, health checkers, or apps doing capability discovery — gets intercepted and immediately answered by this tool, never reaching your own code. If you have written any logic to handle OPTIONS requests yourself, it will never run by default.

What to do: If you have routes that need to respond to OPTIONS requests with custom logic, enable the 'pass OPTIONS through' setting so the tool does not intercept them.

lib/index.js:corsWithOptions
Ordering

If you set up the tool once and later try to change which websites are allowed by modifying the settings object your code holds onto, nothing will change — the tool already copied those settings when it started and ignores later changes. Developers expecting live control over access rules will be silently ignored.

What to do: To change CORS rules at runtime, use the dynamic function form when setting up the tool, rather than trying to modify the original settings object after the fact.

lib/index.js:cors (exported factory)
Domain

The allowed-origins list does an exact character-for-character comparison. A tiny difference — like an extra slash at the end, or uppercase letters in the web address — will cause the tool to reject a browser's request even if the address is effectively the same website. This produces a silent access error that looks just like a genuine block.

What to do: Make sure the origin strings in your allowed list match exactly how browsers will send them — check for trailing slashes and letter case, or use a regular expression pattern if you need flexible matching.

lib/index.js:isOriginAllowed
Contract

If a request arrives without any 'which website is this from' information — which happens with direct server-to-server calls or certain testing tools — and you have configured the tool to reflect the requester's address back, the response will contain a nonsense value instead of a real web address. This is usually harmless but can confuse security scanning tools.

What to do: If your API serves both browser clients and server-to-server calls, use a function-based origin check that handles the case where no origin information is present in the request.

lib/index.js:configureOrigin
Environment

This tool is built specifically for one family of web frameworks. If someone tries to use it with a different framework — or in a plain web server without the expected framework underneath — it will crash with a confusing technical error about a missing method rather than a clear message saying it is incompatible.

What to do: Check that your web server framework is Express or Connect (or a framework that wraps one of them) before adding this tool; if you are using a different framework, look for a CORS package built specifically for it.

lib/index.js:corsWithOptions

Open the standalone hidden-assumptions report for cors →

How Data Flows Through the System

An HTTP request arrives at an Express app that has cors() mounted. The cors() factory was already called at setup time with an options object or a dynamic resolver function; it returned a (req, res, next) closure that captured those options. When a request arrives, the closure either directly calls corsWithOptions() (static options) or invokes the user-supplied function with the request to get per-request options (dynamic path), then calls corsWithOptions(). corsWithOptions() calls six header-builder functions in sequence — configureOrigin, configureMethods, configureCredentials, configureMaxAge, configureAllowedHeaders, configureExposedHeaders — each returning one or two {key, value} HeaderDescriptors. These are collected into a flat array and passed to applyHeaders(), which writes the non-false values to the response via res.setHeader(). The vary() utility is also called directly on the response wherever a Vary header is needed (to safely append rather than overwrite). Finally, if the request is an OPTIONS preflight and preflightContinue is false, the response is ended immediately with the configured optionsSuccessStatus (default 204); otherwise next() is called and Express continues to the application's own route handlers.

  1. Receive and merge options — At middleware-mount time, cors(options) merges the caller-supplied options with the defaults object ({ origin: '*', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', preflightContinue: false, optionsSuccessStatus: 204 }) using object-assign. The result is stored in the closure. If options is a function, the merge happens later per-request. [CorsOptions → CorsOptions] (config: defaults.origin, defaults.methods, defaults.preflightContinue +1)
  2. Resolve dynamic options — If the caller passed a function as options (e.g. cors((req, cb) => cb(null, {origin: req.headers.origin}))), the middleware invokes that function with the current request and a callback. The callback receives the per-request CorsOptions and passes them into corsWithOptions(). Errors from the callback are forwarded to next(err). [IncomingRequest → CorsOptions]
  3. Build header list — corsWithOptions() calls configureOrigin(options, req) — which tests the request's Origin header against the policy and decides the Access-Control-Allow-Origin value — then configureMethods(), configureCredentials(), configureMaxAge(), configureAllowedHeaders() (which may mirror the request's Access-Control-Request-Headers if no explicit list is configured), and configureExposedHeaders(). Each returns an array of HeaderDescriptor objects. These are all concatenated into one flat array. [CorsOptions → HeaderDescriptor] (config: options.origin, options.methods, options.credentials +3)
  4. Apply headers to response — applyHeaders(headers, res) iterates the HeaderDescriptor array. For each entry, if the value is not false or null, it calls res.setHeader(key, value). The vary() library is called separately at the point each Vary header descriptor is built (inside configureOrigin and configureAllowedHeaders), safely appending to any existing Vary header rather than overwriting it. [HeaderDescriptor → ServerResponse]
  5. Short-circuit preflight or continue chain — After headers are applied, corsWithOptions() checks: is req.method === 'OPTIONS' AND options.preflightContinue === false? If yes, sets res.statusCode to options.optionsSuccessStatus (default 204) and calls res.end() — the browser's preflight check is satisfied, no application logic runs. If preflightContinue is true, or if the method is not OPTIONS, next() is called and Express proceeds to the next middleware or route handler. [IncomingRequest] (config: options.preflightContinue, options.optionsSuccessStatus)

Data Models

The data structures that flow between stages — the contracts that hold the system together.

CorsOptions lib/index.js
Plain object with: origin (string | RegExp | Array | boolean | function(req, cb)), methods (string | string[]), allowedHeaders (string | string[]), exposedHeaders (string | string[]), credentials (boolean), maxAge (number), preflightContinue (boolean), optionsSuccessStatus (number). Defaults: origin='*', methods='GET,HEAD,PUT,PATCH,POST,DELETE', preflightContinue=false, optionsSuccessStatus=204.
Passed in by the application developer at cors() call time, merged with defaults via object-assign, and read once per request inside corsWithOptions().
HeaderDescriptor lib/index.js
Object with key: string (e.g. 'Access-Control-Allow-Origin') and value: string | false | null. false/null means 'do not set this header'.
Created by each configure*() function, collected into a flat array by corsWithOptions(), then written to the response object via applyHeaders().
IncomingRequest lib/index.js
Standard Node.js IncomingMessage: req.method (string), req.headers.origin (string | undefined), req.headers['access-control-request-headers'] (string | undefined).
Provided by Express/Connect on each incoming HTTP request; read-only inside the middleware — the middleware never modifies the request.
ServerResponse lib/index.js
Standard Node.js ServerResponse extended by Express: res.setHeader(name, value), res.end(), res.statusCode. The middleware calls vary() on it to append to the Vary header.
Received from Express, mutated by applyHeaders() (sets Access-Control-* headers) and potentially terminated by the preflight short-circuit path.

System Behavior

How the system operates at runtime — where data accumulates, what loops, what waits, and what controls what.

Control Points

Technology Stack

Node.js (runtime)
Runtime environment; the middleware attaches to its built-in IncomingMessage/ServerResponse HTTP objects
object-assign (library)
Merges caller-supplied CorsOptions with the defaults object without mutating the caller's original object
vary (library)
Safely appends field names to the response's Vary header (e.g. 'Origin', 'Access-Control-Request-Headers') without overwriting values set by other middleware
Express (framework)
The middleware host in tests and in documented usage; cors() is compatible with any Connect-style framework
Mocha (testing)
Test runner executing the describe/it blocks across the five test files
supertest (testing)
Fires real HTTP requests against Express apps in-process during tests and asserts on status codes and response headers

Key Components

Explore the interactive analysis

See the full architecture map, data flow, and code patterns visualization.

Analyze on CodeSea

Related Library Repositories

Frequently Asked Questions

What is cors used for?

Adds CORS headers to HTTP responses so browsers allow cross-origin requests expressjs/cors is a 7-component library written in JavaScript. Data flows through 5 distinct pipeline stages. The codebase contains 6 files.

How is cors architected?

cors is organized into 4 architecture layers: Middleware Factory, Header Builders, Origin Matching, Tests. Data flows through 5 distinct pipeline stages. This layered structure keeps concerns separated and modules independent.

How does data flow through cors?

Data moves through 5 stages: Receive and merge options → Resolve dynamic options → Build header list → Apply headers to response → Short-circuit preflight or continue chain. An HTTP request arrives at an Express app that has cors() mounted. The cors() factory was already called at setup time with an options object or a dynamic resolver function; it returned a (req, res, next) closure that captured those options. When a request arrives, the closure either directly calls corsWithOptions() (static options) or invokes the user-supplied function with the request to get per-request options (dynamic path), then calls corsWithOptions(). corsWithOptions() calls six header-builder functions in sequence — configureOrigin, configureMethods, configureCredentials, configureMaxAge, configureAllowedHeaders, configureExposedHeaders — each returning one or two {key, value} HeaderDescriptors. These are collected into a flat array and passed to applyHeaders(), which writes the non-false values to the response via res.setHeader(). The vary() utility is also called directly on the response wherever a Vary header is needed (to safely append rather than overwrite). Finally, if the request is an OPTIONS preflight and preflightContinue is false, the response is ended immediately with the configured optionsSuccessStatus (default 204); otherwise next() is called and Express continues to the application's own route handlers. This pipeline design reflects a complex multi-stage processing system.

What technologies does cors use?

The core stack includes Node.js (Runtime environment; the middleware attaches to its built-in IncomingMessage/ServerResponse HTTP objects), object-assign (Merges caller-supplied CorsOptions with the defaults object without mutating the caller's original object), vary (Safely appends field names to the response's Vary header (e.g. 'Origin', 'Access-Control-Request-Headers') without overwriting values set by other middleware), Express (The middleware host in tests and in documented usage; cors() is compatible with any Connect-style framework), Mocha (Test runner executing the describe/it blocks across the five test files), supertest (Fires real HTTP requests against Express apps in-process during tests and asserts on status codes and response headers). A focused set of dependencies that keeps the build manageable.

What system dynamics does cors have?

cors exhibits 7 control points. These runtime behaviors shape how the system responds to load, failures, and configuration changes.

What design patterns does cors use?

4 design patterns detected: Middleware Factory Pattern, Header Descriptor Pipeline, Safe Vary Header Appending, Defensive Options Cloning.

Analyzed on June 9, 2026 by CodeSea. Written by .