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