Handling Vary: Origin Header Correctly
Resolves browser and CDN caching conflicts triggered by missing or misconfigured Vary: Origin headers during CORS preflight requests.
Key Troubleshooting Focus:
- Root cause of cached preflight failures across multiple requesting origins
- Impact on reverse proxy and CDN cache-key segmentation
- Framework-specific header injection and validation patterns
- Step-by-step cache isolation verification workflow
Understanding Vary: Origin in Preflight Caching
The Vary: Origin response header instructs HTTP caches to segment stored responses based on the requesting Origin header. Without it, caches serve a single preflight response globally — meaning the first origin to populate the cache sets the Access-Control-Allow-Origin value for all subsequent requests.
Browser preflight caches follow the WHATWG Fetch Standard. They isolate OPTIONS responses per origin only when Vary: Origin is explicitly present.
CDNs generate cache keys using request headers listed in Vary. Dynamic origin reflection without Vary causes key collisions. The first cached Access-Control-Allow-Origin value serves all subsequent origins, regardless of whether the origin is authorized.
| Cache State | Vary Header |
Access-Control-Allow-Origin |
Result |
|---|---|---|---|
| Initial Request | Missing | https://app.example.com |
Cached globally |
| Subsequent Request | Missing | https://app.example.com (stale) |
CORS blocked for https://admin.example.com |
| Initial Request | Origin |
https://app.example.com |
Cached per origin |
| Subsequent Request | Origin |
https://admin.example.com |
Correct per-origin cache hit |
Implementing a robust baseline requires aligning server-side logic with Server-Side CORS Configuration & Header Management before introducing edge caching layers.
Exact Console Errors & Root Cause Analysis
Misconfigured Vary headers manifest as intermittent CORS failures. The browser reports policy violations despite valid server-side logic.
Primary Console Error:
Access to XMLHttpRequest at 'https://api.example.com/data' from origin 'https://admin.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Root Cause Mapping:
- The CDN or browser preflight cache returns a stale
OPTIONSresponse. - The cached response contains
Access-Control-Allow-Origin: https://app.example.com. - The requesting origin (
https://admin.example.com) does not match. - The browser rejects the response as a cross-origin violation, not a network error.
Diagnostic Checklist:
- Open Chrome DevTools > Network tab.
- Filter by
PreflightorOPTIONS. - Inspect
Response HeadersforVary: Origin. - Check
Cache-ControlandAgeheaders to confirm stale delivery. - Verify
Access-Control-Allow-Originmatches the exact requesting origin.
Framework & Reverse Proxy Configuration
Correct implementation requires strict header ordering and conditional reflection. Middleware execution order dictates cache segmentation behavior.
Nginx Configuration
Use the map directive for safe origin validation. Place Vary in the location block unconditionally so it is always included regardless of whether the origin is in the allowlist.
map $http_origin $cors_origin {
default "";
~^https://([a-z0-9-]+\.)?example\.com$ $http_origin;
}
server {
location /api/ {
add_header Vary Origin always;
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS' always;
add_header Access-Control-Allow-Headers 'Content-Type, Authorization' always;
add_header Access-Control-Max-Age 600 always;
return 204;
}
proxy_pass http://backend;
}
}
Express.js Middleware
Set Vary unconditionally before evaluating origin allowlists. Execution order prevents race conditions in async middleware chains.
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
app.use((req, res, next) => {
res.set('Vary', 'Origin');
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.set('Access-Control-Allow-Origin', origin);
res.set('Access-Control-Allow-Credentials', 'true');
}
next();
});
Directive precedence and validation rules align with Access-Control-* Header Directives. Cloudflare Workers or Edge Rules must mirror this exact header injection sequence to prevent upstream stripping.
Step-by-Step Validation & Cache Bypass Testing
Verify cache segmentation by simulating concurrent preflight requests from distinct origins. Bypass intermediate caches during initial validation.
cURL Multi-Origin Preflight Simulation:
curl -I -X OPTIONS -H 'Origin: https://app.example.com' -H 'Access-Control-Request-Method: POST' https://api.example.com/data
curl -I -X OPTIONS -H 'Origin: https://admin.example.com' -H 'Access-Control-Request-Method: POST' https://api.example.com/data
Each response should contain Vary: Origin and the correct Access-Control-Allow-Origin matching the request origin.
DevTools Cache-Hit/Miss Verification:
- Disable cache in DevTools Network tab.
- Trigger preflight from Origin A. Verify
Vary: Originand correctAccess-Control-Allow-Origin. - Re-enable cache. Trigger preflight from Origin B.
- Confirm the response
Access-Control-Allow-Originreflects Origin B, not Origin A.
Header Order Validation:
- Reverse proxies often reorder headers during response normalization.
- Use
curl -voropenssl s_clientto inspect raw header transmission order. Varymust be present on every dynamic CORS response regardless of position.
Edge-Case Security Boundary Mapping
Improper Vary scoping introduces cache poisoning vectors in multi-tenant architectures.
Origin Reflection Attack Mitigation:
- Never reflect
$http_originorreq.headers.originwithout allowlist validation. - Pair strict allowlists with
Vary: Originto segment cached responses. - Reject requests with malformed or null origins at the routing layer.
Subdomain Credential Isolation:
Access-Control-Allow-Credentials: truerequires exact origin matching.- Dynamic origin allowlists must enforce TLD+1 boundaries.
- Isolate session cookies per subdomain to prevent cross-origin credential leakage.
Proxy-Level Audit Trail:
- Monitor load balancer logs for stripped
Varyheaders. - Track cache-key collisions using CDN analytics dashboards.
- Implement response header validation in CI/CD pipelines before deployment.
Common Mistakes
| Issue | Technical Impact | Resolution |
|---|---|---|
Omitting Vary: Origin with dynamic Access-Control-Allow-Origin |
Global cache poisoning. First origin’s value serves all subsequent requests. | Add Vary: Origin to all dynamic CORS responses. |
Setting Vary: * to bypass caching |
Disables HTTP caching entirely for that resource. Increases origin latency and server load. | Use Vary: Origin for precise, standards-compliant segmentation. |
Reverse proxy stripping Vary |
CDN treats all preflights as identical cache keys. Cross-origin collisions occur. | Configure proxy pass-through rules to preserve Vary headers. |
FAQ
Does Vary: Origin work with Access-Control-Allow-Origin: *?
There is no need for Vary: Origin when using a wildcard origin — the wildcard is the same value regardless of requesting origin. Vary: Origin is only meaningful when dynamically reflecting specific origins based on the request.
How to prevent CDN cache poisoning via Origin reflection?
Validate the Origin header against a strict allowlist before reflection. Never reflect arbitrary origins, and always pair reflection with Vary: Origin.
Why does Vary: Origin cause 403 errors on cached preflight responses?
A cached response with a stale or incorrect Access-Control-Allow-Origin is served to a new origin. The browser blocks the request, interpreting it as a policy violation rather than a cache miss. The fix is to purge the CDN cache after allowlist changes.