Technical guide
16

Technical guide · Published Jun 12, 2016

Setting Up CORS Vary-Origin Headers in AWS S3

The exact S3 CORS configuration I used to make responses vary by origin instead of letting a CDN cache the wrong header.

A field-guide drawing of a trail backpack, map, and notebook
In this article2 sections

I needed one S3 bucket to serve assets to multiple domains. The request worked for one origin and then failed for another because the cached response carried the first site’s CORS header. The missing piece was Vary: Origin.

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>

In the S3 console, go to the bucket’s Permissions -> CORS configuration and use this XML. The first rule matches http:// and https:// origins and returns the specific origin in Access-Control-Allow-Origin. The second handles simple GET and HEAD requests with * as a fallback. Together, the rules make S3 vary the response 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.

Field 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 separate http* and * rules are what triggered this behavior in my setup.

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.

One quick signal

Did this earn your time?

What was missing?

Thanks. That gives me something concrete to check.