I have a webpage (https://smartystreets.com/contact) that uses jQuery to load some SVG files from S3 through the CloudFront CDN.
In Chrome I will open an Incognito window as well as the console. Then I will load the page. As the page loads, I will typically get 6 to 8 messages in the console that look similar to this:
XMLHttpRequest cannot load
https://d79i1fxsrar4t.cloudfront.net/assets/img/feature-icons/documentation.08e71af6.svg.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'https://smartystreets.com' is therefore not allowed access.
If I do a standard reload of the page, even multiple time, I continue to get the same errors. If I do Command+Shift+R
then most, and sometimes all, of the images will load without the XMLHttpRequest
error.
Sometimes even after the images have loaded, I will refresh and one or more of the images will not load and return that XMLHttpRequest
error again.
I have checked, changed, and re-checked the settings on S3 and Cloudfront. In S3 my CORS configuration looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedOrigin>http://*</AllowedOrigin>
<AllowedOrigin>https://*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
</CORSConfiguration>
(Note: initially had only <AllowedOrigin>*</AllowedOrigin>
, same problem.)
In CloudFront the distribution behavior is set to allow the HTTP Methods: GET, HEAD, OPTIONS
. Cached methods are the same. Forward Headers is set to "Whitelist" and that whitelist includes, "Access-Control-Request-Headers, Access-Control-Request-Method, Origin".
The fact that it works after a cache-less browser reload seems to indicate that all is well on the S3/CloudFront side, else why would the content be delivered. But then why would the content not be delivered on the initial page-view?
I am working in Google Chrome on macOS. Firefox has no problem getting the files every time. Opera NEVER gets the files. Safari will pick up the images after several refreshes.
Using curl
I do not get any problems:
curl -I -H 'Origin: smartystreets.com' https://d79i1fxsrar4t.cloudfront.net/assets/img/phone-icon-outline.dc7e4079.svg
HTTP/1.1 200 OK
Content-Type: image/svg+xml
Content-Length: 508
Connection: keep-alive
Date: Tue, 20 Jun 2017 17:35:57 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Access-Control-Max-Age: 3000
Last-Modified: Thu, 15 Jun 2017 16:02:19 GMT
ETag: "dc7e4079f937e83291f2174853adb564"
Cache-Control: max-age=31536000
Expires: Wed, 01 Jan 2020 23:59:59 GMT
Accept-Ranges: bytes
Server: AmazonS3
Vary: Origin,Access-Control-Request-Headers,Access-Control-Request-Method
Age: 4373
X-Cache: Hit from cloudfront
Via: 1.1 09fc52f58485a5da8e63d1ea27596895.cloudfront.net (CloudFront)
X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g==
Some have suggested that I delete the CloudFront distribution and recreate it. Seems like a rather harsh and inconvenient fix.
What is causing this problem?
Update:
Adding response headers from an image that failed to load.
age:1709
cache-control:max-age=31536000
content-encoding:gzip
content-type:image/svg+xml
date:Tue, 20 Jun 2017 17:27:17 GMT
expires:2020-01-01T23:59:59.999Z
last-modified:Tue, 11 Apr 2017 18:17:41 GMT
server:AmazonS3
status:200
vary:Accept-Encoding
via:1.1 022c901b294fedd7074704d46fce9819.cloudfront.net (CloudFront)
x-amz-cf-id:i0PfeopzJdwhPAKoHpbCTUj1JOMXv4TaBgo7wrQ3TW9Kq_4Bx0k_pQ==
x-cache:Hit from cloudfront
You're making two requests for the same object, one from HTML, one from XHR. The second one fails, because Chrome uses the cached response from the first request, which has no
Access-Control-Allow-Origin
response header.Why?
Chromium bug 409090 Cross-origin request from cache failing after regular request is cached describes this problem, and it's a "won't fix" -- they believe their behavior is correct. Chrome considers the cached response to be usable, apparently because the response didn't include a
Vary: Origin
header.But S3 does not return
Vary: Origin
when an object is requested without anOrigin:
request header, even when CORS is configured on the bucket.Vary: Origin
is only sent when anOrigin
header is present in the request.And CloudFront does not add
Vary: Origin
even whenOrigin
is whitelisted for forwarding, which should by definition mean that varying the header might modify the response -- that's the reason why you forward and cache against request headers.CloudFront gets a pass, because its response would be correct if S3's were more correct, since CloudFront does return this when it's provided by S3.
S3, a little fuzzier. It is not wrong to return
Vary: Some-Header
when there was noSome-Header
in the request.Clearly,
Vary: Some-Absent-Header
is valid, so S3 would be correct if it addedVary: Origin
to its response if CORS is configured, since that indeed could vary the response.And, apparently, this would make Chrome do the right thing. Or, if it doesn't do the right thing in this case, it would be violating a
MUST NOT
. From the same section:So, S3 really
SHOULD
be returningVary: Origin
when CORS is configured on the bucket, ifOrigin
is absent from the request, but it doesn't.Still, S3 is not strictly wrong for not returning the header, because it's only a
SHOULD
, not aMUST
. Again, from the same section of RFC-7231:On the other hand, the argument could be made that Chrome should implicitly know that varying the
Origin
header should be a cache key because it could change the response in the same wayAuthorization
could change the response.Similarly, reuse across origins is arguably constrained by the nature of
Origin
but this argument is not a strong one.tl;dr: You apparently cannot successfully fetch an object from HTML and then successfully fetch it again with as a CORS request with Chrome and S3 (with or without CloudFront), due to peculiarities in the implementations.
Workaround:
This behavior can be worked-around with CloudFront and Lambda@Edge, using the following code as an Origin Response trigger.
This adds
Vary: Access-Control-Request-Headers, Access-Control-Request-Method, Origin
to any response from S3 that has noVary
header. Otherwise, theVary
header in the response is not modified.Attribution: I am also the author of the original post on the AWS Support forums where this code was initially shared.
The Lambda@Edge solution above results in fully correct behavior, but here are two alternatives that you may find useful, depending on your specific needs:
Alternative/Hackaround #1: Forge the CORS headers in CloudFront.
CloudFront supports custom headers that are added to each request. If you set
Origin:
on every request, even those that are not cross-origin, this will enable correct behavior in S3. The configuration option is called Custom Origin Headers, with the word "Origin" meaning something entirely different than it means in CORS. Configuring a custom header like this in CloudFront overwrites what is sent in the request with the value specified, or adds it if absent. If you have exactly one origin accessing your content over XHR, e.g.https://example.com
, you can add that. Using*
is dubious, but might work for other scenarios. Consider the implications carefully.Alternative/Hackaround #2: Use a "dummy" query string parameter that differs for HTML and XHR or is absent from one or the other. These parameters are typically named
x-*
but should not bex-amz-*
.Let's say you make up the name
x-request
. So<img src="https://dzczcexample.cloudfront.net/image.png?x-request=html">
. When accessing the object from JS, don't add the query parameter. CloudFront is already doing the right thing, by caching different versions of the objects using theOrigin
header or absence of it as part of the cache key, because you forwarded that header in your cache behavior. The problem is, your browser doesn't know this. This convinces the browser that this is actually a separate object that needs to be requested again, in a CORS context.If you use these alternative suggestions, use one or the other -- not both.
As of Nov 2021, CloudFront directly supports Response Headers Policies. These include CORS, security and custom headers. There is no need to inject custom headers via Lambda@Edge or CloudFront Functions anymore.
Perhaps more pleasingly, there is no need to add Vary as a custom header any more. The new CORS implementation in Headers Polices includes additional logic to set the appropriate headers such as Vary according to the fetch standard.
I don't know why you'd be getting such different results from various browsers, but:
That line right there is what (if you can get their attention) a CloudFront or Support engineer will use to follow one of your failed requests. If the request is getting to a CloudFront server, it should have this header in the response. If that header isn't there, then the request is likely failing somewhere before it gets to CloudFront.
The accepted solution addresses the issue, but it's not the most performant, particularly for CloudFront distributions that serve dynamic content. Setting up header-caching with a whitelist results in CloudFront caching multiple versions of the requested object depending on the header. This means that internally CloudFront may need to refetch the object from the S3 origin multiple times. Data transfer from S3 to CloudFront is free, but that doesn't account for additional latency.
An alternative solution here would be to disable CORS configuration on the S3 bucket, and instead manually set CORS headers using a Lambda@Edge function configured on the viewer response. The function could look as follows:
There is another simpler solution that works for me with the use of a HTML attribute called
crossorigin='anonymous'
as detailed here. Basically you can add this attribute as such:<img src="your_image_url_here" crossorigin='anonymous'>
and this would essentially makes your "first" request to the image a CORS request, now if you try to retrieve the same image again through XHR, even if Chrome decides to use the cache (cached response for the first request) it will be fine as it will now comes with
Access-Control-Allow-Origin
header.I didn't have the reputation to comment on the accepted answer but wanted to help anyone else experiencing similar issues.
In short, I believe AWS have changed something so that the accepted solution's lambda code no longer works (perhaps if/when you switch to CloudFront's new Cache Policy implementation?)
headers['vary']
is not falsey so the workaround is never triggered.This is the fixed lambda as per dobesv's solution in the original forum post ( https://forums.aws.amazon.com/thread.jspa?messageID=796312):
To avoid the caching behavior altogether, you can append a random query parameter when doing the request via XHR.