If you’re displaying images stored in an AWS S3 private bucket using signed URLs, you might have encountered a confusing scenario: images display perfectly in your web pages but throw CORS errors when you try to download them using JavaScript’s fetch
API.
Let’s break down the issue and explore a practical solution.
Understanding the Problem
Imagine you have your images securely stored in a private S3 bucket, and you’re using pre-signed URLs to grant temporary access:
<img src="https://s3.eu-central-1.amazonaws.com/my.bucket/image.png?...signed_url_params" />
The above works flawlessly; your images render perfectly. You can even open these URLs directly in another browser tab.
However, when attempting to fetch these images with JavaScript:
fetch('https://s3.eu-central-1.amazonaws.com/my.bucket/image.png?...signed_url_params')
.then(response => response.blob())
.then(blob => {
// handle download
});
You suddenly encounter the dreaded error:
Access to fetch at '...' from origin 'https://yourdomain.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Why does this happen?
What’s Causing the CORS Error?
This issue typically occurs due to how browsers (particularly Chrome) handle caching with AWS S3 and signed URLs:
- When an image URL is initially loaded in a non-CORS context (such as via an
<img>
tag), Chrome caches the response without any CORS headers. - Later, when fetching the same URL via JavaScript (
fetch
API), Chrome tries to reuse this cached response. - Because the cached version didn’t originally include
Access-Control-Allow-Origin
headers (since they weren’t necessary at the time), Chrome now fails the CORS check.
Ideally, AWS S3 should respond with a Vary: Origin
header to indicate different responses might be needed based on the requesting origin. However, it currently doesn’t provide this header consistently when the initial request doesn’t include an Origin
header.
Practical Solution: Avoid Browser Caching
To fix this issue, the simplest method is to prevent the browser from caching the initial response. This ensures Chrome fetches a fresh copy of the image every time, correctly including the necessary CORS headers.
Here are three effective ways:
Method 1: Add Cache-Control: no-cache
to S3 objects
Set the Cache-Control
metadata of your images in S3 to no-cache
. This forces browsers to always fetch a fresh copy of the object.
Method 2: Use Query Parameter response-cache-control=no-cache
When generating your signed URLs, include the special AWS S3 query parameter:
const signedUrl = s3.getSignedUrl('getObject', {
Bucket: 'my.bucket',
Key: 'image.png',
Expires: 900,
ResponseCacheControl: 'no-cache' // Add this!
});
This way, AWS automatically includes the Cache-Control: no-cache
header in its response, bypassing the cache issue.
Method 3: Generate Different Signed URLs
Create two slightly different signed URLs for the same object, such as by varying their expiration times. Because the URLs differ, Chrome treats them as separate objects, thus avoiding the problematic cached response.
// First URL
const url1 = s3.getSignedUrl('getObject', { Bucket, Key, Expires: 900 });
// Second URL, slightly different expiration
const url2 = s3.getSignedUrl('getObject', { Bucket, Key, Expires: 960 });
If you are facing CORS issues, without fetch also, or you are not loading image with <img> tag, you should check the bucket policy, it should have values like this.
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedOrigins": [
"https://app.example.com",
"https://qa.example.com",
"https://staging.example.net",
"https://main.example.com",
"https://clientapp.example.org"
],
"ExposeHeaders": [],
"MaxAgeSeconds": 3000
}
]
References: Chromium bug – https://issues.chromium.org/issues/40381978
Conclusion
By using these simple yet effective solutions, you can avoid frustrating CORS errors caused by Chrome’s caching behavior with AWS S3 signed URLs.
Choose the method that best aligns with your workflow, and you’ll have your fetch-based downloads working smoothly again.