expressjs/cors
Node.js CORS middleware
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".
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.
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)
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
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)
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
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
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.
- 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)
- 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]
- 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)
- 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]
- 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.
lib/index.jsPlain 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().
lib/index.jsObject 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().
lib/index.jsStandard 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.
lib/index.jsStandard 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
- origin (runtime-toggle) — Controls: Which origins are allowed. '*' permits all origins with no reflection. A string sets a fixed allowed origin. A RegExp or array enables pattern/multi-origin matching. A boolean true reflects any requesting origin. A function enables fully dynamic per-request policy. Default: '*'.. Default: '*'
- methods (runtime-toggle) — Controls: The value of Access-Control-Allow-Methods — which HTTP verbs the browser is told it can use cross-origin. Can be a comma-string or an array. Default: 'GET,HEAD,PUT,PATCH,POST,DELETE'.. Default: 'GET,HEAD,PUT,PATCH,POST,DELETE'
- preflightContinue (feature-flag) — Controls: Whether OPTIONS preflight requests are terminated by this middleware (false = respond immediately with optionsSuccessStatus, the common case) or passed to the next handler in the chain (true = the app handles the OPTIONS response itself). Default: false.. Default: false
- optionsSuccessStatus (runtime-toggle) — Controls: The HTTP status code sent to end preflight (OPTIONS) requests when preflightContinue is false. 204 is standard; 200 is recommended for IE11 and some SmartTV browsers that choke on 204. Default: 204.. Default: 204
- credentials (runtime-toggle) — Controls: Whether Access-Control-Allow-Credentials: true is added. Required for cross-origin requests that send cookies or HTTP auth. Must not be combined with origin:'*' (browsers reject that combination). Default: not set (header omitted).
- maxAge (runtime-toggle) — Controls: Sets Access-Control-Max-Age, telling the browser how many seconds to cache the preflight response before issuing another OPTIONS request. Default: not set (header omitted, browser uses its own default).
- allowedHeaders (runtime-toggle) — Controls: Sets Access-Control-Allow-Headers. When omitted, the middleware mirrors the request's Access-Control-Request-Headers value, allowing whatever headers the browser asks for. When set, restricts headers to the declared list. Default: not set (mirror mode).
Technology Stack
Runtime environment; the middleware attaches to its built-in IncomingMessage/ServerResponse HTTP objects
Merges caller-supplied CorsOptions with the defaults object without mutating the caller's original object
Safely appends field names to the response's Vary header (e.g. 'Origin', 'Access-Control-Request-Headers') without overwriting values set by other middleware
The middleware host in tests and in documented usage; cors() is compatible with any Connect-style framework
Test runner executing the describe/it blocks across the five test files
Fires real HTTP requests against Express apps in-process during tests and asserts on status codes and response headers
Key Components
- cors (exported factory) (factory) — The public API. Takes a CorsOptions object or a function(req, callback) for dynamic options. If static options are given, returns a middleware that calls corsWithOptions() directly. If a function is given, wraps it so the function is invoked per-request and its result is passed to corsWithOptions(). This is the only symbol exported by the package.
lib/index.js - corsWithOptions (orchestrator) — Core per-request logic. Calls all the configure*() header builders in sequence, flattens the resulting HeaderDescriptors into a single array, and calls applyHeaders(). Then branches: if the request is an OPTIONS preflight and preflightContinue is false, it ends the response with optionsSuccessStatus; otherwise it calls next().
lib/index.js - configureOrigin (processor) — Decides the value of Access-Control-Allow-Origin. For origin='*' → sets '*'. For a fixed string → sets that string and adds Vary: Origin. For anything else (RegExp, array, boolean) → calls isOriginAllowed() against the request's Origin header; if allowed, reflects the request's own Origin value back (and adds Vary: Origin); if not, sets value to false (header omitted).
lib/index.js - isOriginAllowed (resolver) — Recursive origin checker. Handles four cases: array (recurses on each element), string (exact equality), RegExp (regex test), anything else (truthy coercion). Returns boolean. Called by configureOrigin() when a non-wildcard, non-string origin policy is configured.
lib/index.js - configureMethods (processor) — Converts options.methods (either a string like 'GET,POST' or an array ['GET','POST']) into a single comma-joined string and returns an Access-Control-Allow-Methods HeaderDescriptor.
lib/index.js - configureAllowedHeaders (processor) — Sets Access-Control-Allow-Headers. If options.allowedHeaders is not explicitly set, it mirrors the browser's Access-Control-Request-Headers value from the request (and adds Vary: Access-Control-Request-Headers). If explicitly set, normalizes array→string and uses that.
lib/index.js - applyHeaders (adapter) — Iterates over the flat array of HeaderDescriptors produced by corsWithOptions() and calls res.setHeader() for each one whose value is not false or null. This is the only place the response object is actually mutated.
lib/index.js
Explore the interactive analysis
See the full architecture map, data flow, and code patterns visualization.
Analyze on CodeSeaRelated 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 Karolina Sarna.