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:

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:

Diagnostic Checklist:

  1. Open Chrome DevTools > Network tab.
  2. Filter by Preflight or OPTIONS.
  3. Inspect Response Headers for Vary: Origin.
  4. Check Cache-Control and Age headers to confirm stale delivery.
  5. Verify Access-Control-Allow-Origin matches 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:

  1. Disable cache in DevTools Network tab.
  2. Trigger preflight from Origin A. Verify Vary: Origin and correct Access-Control-Allow-Origin.
  3. Re-enable cache. Trigger preflight from Origin B.
  4. Confirm the response Access-Control-Allow-Origin reflects Origin B, not Origin A.

Header Order Validation:

Edge-Case Security Boundary Mapping

Improper Vary scoping introduces cache poisoning vectors in multi-tenant architectures.

Origin Reflection Attack Mitigation:

Subdomain Credential Isolation:

Proxy-Level Audit Trail:

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.