LEARNING OBJECTIVES โต
- Master the complete grammar, values, and constraints of all six standard CORS response headers.
- Understand the strict browser rule prohibiting
Access-Control-Allow-Origin: *when credentials are included. - Learn how to expose custom response headers to frontend JavaScript using
Access-Control-Expose-Headers. - Architect robust, compliant header generation pipelines for production web services and microfrontends.
๐ The Mental Model & Story (Intuitive Foundation)
Imagine a customs officer at an international airport arrivals checkpoint. You arrive holding a locked diplomatic briefcase containing financial records, with a signed letter from your employer.
+---------------------------------------------------------------------------------------------------+
| THE AIRPORT CUSTOMS PROTOCOL |
+---------------------------------------------------------------------------------------------------+
| 1. Access-Control-Allow-Origin: "Who is allowed to enter?" |
| -> "Only passengers from Origin: https://hq.corp.com" (or "*" for any public tourist). |
| |
| 2. Access-Control-Allow-Credentials: "Are secret diplomatic ID badges / cookies accepted?" |
| -> "true": Yes, private session tokens and authentication cookies are accepted. |
| -> ๐จ CRITICAL RULE: If diplomatic badges are accepted, "*" is FORBIDDEN! Specific names only!|
| |
| 3. Access-Control-Allow-Methods: "What actions can the visitor take?" |
| -> "GET, POST, PUT, DELETE, PATCH" |
| |
| 4. Access-Control-Allow-Headers: "What custom security seals may the visitor bring?" |
| -> "Authorization, X-Tenant-Id, Content-Type" |
| |
| 5. Access-Control-Expose-Headers: "Which internal document seals can the visitor read back?" |
| -> By default, visitors can only read basic seals (Content-Type, Cache-Control). |
| -> Must declare "X-Total-Count, Content-Range" to allow JavaScript inspection! |
+---------------------------------------------------------------------------------------------------+
Each Access-Control-* header serves as an explicit clause in a legal contract between the origin server and the browser's security runtime. If a server omits an essential clause or attempts an invalid combination (like trying to accept credentials while granting universal wildcard access), the browser immediately tears up the contract and denies JavaScript access to the resource.
Technical Deep Dive & Specifications
The Complete CORS Response Headers Suite
Modern browsers evaluate six standardized CORS response headers defined in the W3C / WHATWG Fetch Living Standard:
+------------------------------------------------------------------------------------+
| THE SIX CORS RESPONSE HEADERS |
+------------------------------------------------------------------------------------+
| 1. Access-Control-Allow-Origin <origin> | * |
| 2. Access-Control-Allow-Credentials true |
| 3. Access-Control-Allow-Methods <method>[, <method>]* |
| 4. Access-Control-Allow-Headers <header-name>[, <header-name>]* |
| 5. Access-Control-Expose-Headers <header-name>[, <header-name>]* | * |
| 6. Access-Control-Max-Age <delta-seconds> |
+------------------------------------------------------------------------------------+
Detailed Header Specifications & Rules
1. Access-Control-Allow-Origin
Specifies which origin(s) can access the resource.
- Syntax:
Access-Control-Allow-Origin: https://client.example.comORAccess-Control-Allow-Origin: * - Specification Rule: It only accepts a single origin string or the wildcard literal
*. It does not support multiple comma-separated origins or wildcard subdomains like*.example.com.
2. Access-Control-Allow-Credentials
Indicates whether the response can be exposed to frontend JavaScript when the request's credentials mode is include (e.g. cookies, HTTP authentication, or TLS client certificates).
- Syntax:
Access-Control-Allow-Credentials: true - The Incompatibility Invariant: If a request includes credentials (
credentials: 'include'), the server MUST NOT specifyAccess-Control-Allow-Origin: *. It must specify the exact, verified requesting origin (e.g.Access-Control-Allow-Origin: https://app.example.com). If*is sent, the browser aborts the request with a security error.
+------------------------------------------------------------------------------------+
| ๐ FORBIDDEN COMBINATION (FATAL CORS ERROR): |
| |
| Access-Control-Allow-Origin: * |
| Access-Control-Allow-Credentials: true |
| |
| ==> Chrome/Firefox/Safari ERROR: "The value of the 'Access-Control-Allow-Origin' |
| header in the response must not be the wildcard '*' when the request's |
| credentials mode is 'include'." |
+------------------------------------------------------------------------------------+
3. Access-Control-Allow-Methods
Used in response to a preflight OPTIONS request to indicate which HTTP methods are permitted for the actual request.
- Syntax:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
4. Access-Control-Allow-Headers
Used in response to a preflight OPTIONS request to indicate which HTTP headers can be used during the actual request.
- Syntax:
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-Api-Key
5. Access-Control-Expose-Headers
By default, cross-origin JavaScript can only read CORS-safelisted response headers:
Cache-ControlContent-LanguageContent-LengthContent-TypeExpiresLast-ModifiedPragma
If your API sends pagination or tracing headers (e.g., X-Total-Count: 1540, Content-Range: items 0-49/1540, X-Request-Id), calling response.headers.get('X-Total-Count') in JavaScript returns null unless the server explicitly lists them in Access-Control-Expose-Headers.
- Syntax:
Access-Control-Expose-Headers: Content-Range, X-Total-Count, X-Request-Id
6. Access-Control-Max-Age
Defines how many seconds the results of a preflight request can be cached in the browser's preflight cache.
- Syntax:
Access-Control-Max-Age: 86400(24 hours)
๐ป Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Lines 50โ57: Extracts the developer's selected policy options for
Origin,Credentials,Expose-Headers, andMax-Age. - Lines 62โ64: Enforces the WHATWG Fetch specification constraint: if
Credentials: true,Allow-Origin: *is strictly illegal and causes a browser-level rejection. - Lines 67โ69: Flags comma-separated origins. Many developers mistakenly believe
Access-Control-Allow-Origin: https://a.com, https://b.comis valid, but the spec only permits a single origin. - Lines 72โ74: Checks the preflight cache duration against real-world browser limits (e.g. Chrome's 7200-second maximum cap).
Expected Browser Render Output
โ๏ธ Server CORS Header Configuration ๐ Browser Evaluation & Audit
[ Allow-Origin: Wildcard (*) ] === WHATWG FETCH SPECIFICATION AUDIT ===
[ Allow-Credentials: true ] Status: โ INVALID / BROKEN POLICY
[ Expose: X-Total-Count ]
[ Max-Age: 3600 ] ๐ด SPECIFICATION VIOLATIONS:
โข CRITICAL: Access-Control-Allow-Origin
[ Validate Header Policy Compliance ] cannot be '*' when Access-Control-
Allow-Credentials is 'true'.๐๏ธ Hands-On Exercise
๐ฏ The Challenge: Build a Multi-Tenant CORS Header Generator
Instructions:
- Implement a function
generateCorsHeaders(requestOrigin, isPreflight, config)whereconfigcontains:allowedOrigins: array of allowed origin strings.supportsCredentials: boolean.exposedHeaders: array of string header names.maxAge: integer seconds.
- If
supportsCredentialsistrue, ensure that even ifallowedOriginsincludes'*', the function never outputsAccess-Control-Allow-Origin: *; it must reflect the matchingrequestOrigin. - If
isPreflightistrue, includeAccess-Control-Allow-Methods,Access-Control-Allow-Headers, andAccess-Control-Max-Age. - Ensure
Access-Control-Expose-Headersis included on non-preflight responses whenexposedHeadersis populated.
๐ Starter Code Sandbox
โ ๏ธ Common Pitfalls
- Sending Comma-Separated Origins:
Access-Control-Allow-Origin: https://a.com, https://b.comis completely invalid. The specification requires exactly one origin or*. To support multiple origins, dynamically inspect theOriginheader and reflect it if whitelisted. - Forgetting
Access-Control-Expose-Headersfor Pagination: Writing pagination APIs that returnX-Total-CountorContent-Rangewithout exposing them. The client will receive the response, butresponse.headers.get('X-Total-Count')will returnnull. - Pairing
*withcredentials: 'include': Configuring your server withAccess-Control-Allow-Origin: *while your client issuesfetch(url, { credentials: 'include' }). This combination is blocked by the browser by design.
๐ก Pro Tips
- Use
Access-Control-Expose-Headers: *for Public APIs: In modern browsers (Fetch specification Level 2), you can configureAccess-Control-Expose-Headers: *on public unauthenticated endpoints to expose all custom response headers automatically without listing them individually. - Combine
Access-Control-Max-Agewith HTTP Caching: Ensure your reverse proxy (Nginx/CloudFront) caches the preflight response with properCache-Controlso the reverse proxy itself answers subsequent preflights without invoking backend containers.
๐ Key Takeaways
Access-Control-Allow-Originaccepts either a single specific origin or*; comma-separated lists are invalid.Access-Control-Allow-Credentials: trueandAccess-Control-Allow-Origin: *are mutually exclusive in the browser security model.- Frontend JavaScript cannot read custom response headers (e.g.
X-Total-Count) unless the server lists them inAccess-Control-Expose-Headers. Access-Control-Allow-MethodsandAccess-Control-Allow-Headersare exclusively returned in response to preflightOPTIONSrequests.Access-Control-Max-Agecaches preflight permissions in the client browser (up to browser-defined caps, typically 2hโ24h).- --