KahWee - Web Development, AI Tools & Tech Trends

Expert takes on AI tools like Claude and Sora, modern web development with React and Vite, and tech trends. By KahWee.

Setting Up CORS Vary-Origin Headers in AWS S3

When serving files from S3 to multiple domains, you need the Vary: Origin header. 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 example.com and another.com both use. S3 returns Access-Control-Allow-Origin: example.com to the first request. A CDN caches that response. When another.com requests the same asset, it gets the cached response with the wrong origin, and the browser blocks it. Vary: Origin tells caches: "This response changes based on the Origin header. Don't reuse it for different origins."

The configuration

S3 generates Vary: Origin automatically when you configure multiple CORS rules:

<?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>

Go to your S3 bucket -> Permissions -> CORS configuration and paste this XML. The first rule handles preflight requests. It matches http* origins—meaning http:// or https:// URLs—and returns the specific origin 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.

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 Vary: Origin is still present. Caches will still differentiate responses by origin.

Tip

S3 only emits Vary: Origin when your CORS configuration has multiple rules. A single AllowedOrigin: * rule won't produce it, breaking CDN caching for multi-origin setups.

The http* and * combination in separate rules triggers this behavior.

You need this when multiple sites load assets from the same S3 bucket, you're using a CDN in front of S3, or you support credentials in cross-origin requests. If your assets are public and only used by one site, a simpler AllowedOrigin: * config works fine.

For a deeper dive into how CORS works and how to configure it 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.