CORS issue due to aggressive Chrome cache
June 19, 2021
So… I’ve this application where the customer asked to add a snapshot feature of an output result. I was worried about this feature request, because the output is not a canvas but just a DOM node with HTML stuff inside, then I found this dom-to-image lib which works like a charm.
The general idea of the lib is:
- clone the whole node content
- compute styles, add font and images…
- serialize cloned node
- embed everything inside an SVG
- export to PNG
After the first iteration we found a small bug due to CORS limitation in one of our deployment.
Although the data (in our case: images from a WMS service) displayed inside the output widget always origin from a main domain like official.domain.eu, the application is also hosted on other domains like external.domain.net.
Everything was working from official.domain.eu but as soon as snapshot feature landed on external.domain.net we received a bug report: the snapshot was not working there.
Inspecting the network tab we found that a CORS failure: when the snapshot procedure is activated, a series of XHR requests are performed. As requests target official.domain.eu we need CORS to be enabled to make it works.
I’m not sure why the lib is using AJAX requests for downloading these resources but we have the control of the NGINX server so it’s quite easy to enabled CORS for external.domain.net.
add_header 'Access-Control-Allow-Origin' $origin;
After the change above the issue has been fixed in Firefox but not in Chrome. 😣 How can this be possible?
The Chrome console still complains about the missing header:
Access to XMLHttpRequest at 'https://official.domain.eu/somewhere/something' from origin 'external.domain.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
And it’s right: the header is missing, while it’s there in Firefox. 🤯
To understand the issue let’s recap how the snapshot feature works.
- We display a “widget” on the page, inside the DOM
- The widget structure requires the download of additional resources, like images (first set of requests)
- The snapshot feature (roughly speaking) clones the DOM content, and performs additional XHR requests to the same resources (second set of requests)
What is the difference between the first and second set of requests as they are calling the very same resources?
This header is used to:
- validate the origin on the server
- fill the proper
Access-Control-Allow-Origin(see NGINX rule above).
Finally: remember we are inside a browser environment, so every request can be cached.
Let’s take an example resource like https://official.domain.eu/somewhere/something.
The first time Firefox opens the application, it will display the resource on the DOM so it performs a request. As this is not AJAX, but just an
<img> tag, our request is missing the
Origin header and the response will miss the
When we click on the snapshot button it will download the resource again, ignoring the cache as this new request is a cross-origin ones and will include the
Origin header (so it’s formally a different request).
This new response will contains the
If we reload the page things continue to work properly even if this time both requests are resolved from the cache (two different cache entries I guess).
Let see how Chrome 91 tries be “smart”.
The first time Chrome opens the application it behave like Firefox: the image is downloaded from the network.
Origin header in request and no
Access-Control-Allow-Origin in response.
But when we click on the snapshot button Chrome acts differently: it recognize the resource is inside the cache (filled with the request above) and it tries to use it.
So our AJAX request gets back (from the cache) a response without the
Access-Control-Allow-Origin and we get the CORS error.
It seems that Chrome is reusing the same request from the cache although the request should be a different ones.
If we disable the Chrome cache completely, everything works properly.
We can easily solve the issue by providing:
Access-Control-Allow-Origin: *, or…
- an additional
Vary: Originheader (see
The last one is preferred in our case.
It’s not the first time in my experience that
Vary header fixed an issue with cache.
Generally speaking: the browser cache technology is rock solid and implemented by every browser down to IE 9 probably (and no “polyfill” needed!).
Sometimes you don’t need a new CMS, or migrate to a static site generator: just learn how cache works.