Setting Up CORS Vary-Origin Headers in AWS S3
When serving files from S3 to multiple domains, you need the Vary: Origin header to handle caching correctly. Without it, browsers and CDNs cache the first CORS response and serve it to all origins—breaking cross-origin requests for other sites.
Say you have assets in S3 that two sites use: example.com and another.com. If S3 returns Access-Control-Allow-Origin: example.com to the first request, a CDN might cache that response. When another.com requests the same asset, it gets the cached response with the wrong origin, and the browser blocks it. The Vary: Origin header tells caches: "This response changes based on the Origin header, so don't reuse it for different origins."
The configuration
S3 generates Vary: Origin automatically when you configure multiple CORS rules. Here's what works:
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>http*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
</CORSRule>
</CORSConfiguration>
To add this, go to your S3 bucket → Permissions → CORS configuration, and paste the XML. The first rule handles preflight requests (when the browser sends Access-Control-Request-Method). It matches http* origins—meaning http:// or https:// URLs—and returns the specific origin back in Access-Control-Allow-Origin. The second rule handles simple requests (GET/HEAD without preflight) using * as a fallback. Both rules together tell S3 to vary responses by origin, which adds the Vary: Origin header automatically.
Testing it
Send a preflight-style request with both headers:
curl -sI \
-H "Origin: https://kw.sg" \
-H "Access-Control-Request-Method: GET" \
https://s3.amazonaws.com/animate-vpaid-bridge/sample-1.xml
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://kw.sg
Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method
Notice Access-Control-Allow-Origin: https://kw.sg (the specific origin) and Vary: Origin. Now send a simple request with just the Origin header:
curl -sI \
-H "Origin: https://kw.sg" \
https://s3.amazonaws.com/animate-vpaid-bridge/sample-1.xml
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method
Now it returns Access-Control-Allow-Origin: * (wildcard), but the Vary: Origin header is still there. This tells caches to differentiate responses by origin even when returning *.
S3 adds Vary: Origin when your CORS config has multiple rules that could match different request patterns. The combination of http* and * in separate rules triggers this behavior. Without multiple rules, S3 might return a fixed CORS header without Vary, breaking caching for multi-origin use cases.
You need this configuration when multiple sites load assets from the same S3 bucket, you're using a CDN in front of S3, or you support credentials (cookies or auth headers) in cross-origin requests. If your assets are public and only used by one site, a simpler CORS config with AllowedOrigin: * works fine.
For a deeper dive into how CORS works and how to configure it properly in Apache and nginx, see my post on understanding CORS basics.
In 2025, I moved away from S3 to Cloudflare Workers for static hosting, which handles CORS configuration more simply and deploys to the edge globally.