I have set up a server where Flask is running behind Nginx. I plan on using Nginx soon for static resources, however, I was interested in getting caching to work while serving resources from Flask. I have been able to get caching to work in Chrome, Chromium, and Firefox on Windows, Linux, and MacOS. However, caching does not work at all for some reason in Safari on MacOS and in all browsers on iOS.
I have tried many variations of headers, and no matter what I do, I can't get it to work. I have compared headers from other websites where the caching seems to work, and have also tried to replicate it, and it still doesn't help. I have added an Expires header (which doesn't seem to be necessary when Cache-Control is specified), I have added "immutable" to Cache-Control, I have removed the ETag all together, etc. Whether I compress the SVGs or not, it also doesn't seem to make a difference.
Here is how I add compression in Flask:
app_instance = Flask(__name__, static_folder="static", static_url_path="")
Compress(app_instance)
app_instance.config["COMPRESS_MIMETYPES"].append("image/svg+xml")
And here is how I modify relevant headers to help with caching:
@app.after_request
def after_request(response):
ext = request.path.split(".")[-1]
if ext in ["png", "svg", "ttf", "jpg"]:
response.cache_control.public = True
response.cache_control.max_age = 60
response.cache_control.no_cache = None
if ext == "svg":
# before it looked like "image/svg+xml; charset=utf-8"
response.content_type = "image/svg+xml"
response.headers["Strict-Transport-Security"] = "max-age=63072000"
return response
Here is an example of an initial request in Safari on MacOS where caching does not work subsequently:
GET /images/products/59.svg HTTP/1.1
Accept: image/webp,image/avif,image/jxl,image/heic,image/heic-sequence,video/*;q=0.8,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Host: www.<my-domain>.com
Referer: https://www.<my-domain>.com/
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1.1 Safari/605.1.15
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
Connection: keep-alive
Content-Disposition: inline; filename=59.svg
Content-Encoding: br
Content-Length: 3606
Content-Type: image/svg+xml
Date: Tue, 31 Dec 2024 14:28:22 GMT
ETag: "1735522441.7048287-18829-3530625642:br"
Last-Modified: Mon, 30 Dec 2024 01:34:01 GMT
Server: nginx/1.22.1
Strict-Transport-Security: max-age=63072000
Vary: Accept-Encoding
Here is an example of an initial request in Chrome on Windows where caching works subsequently:
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.9,lt-LT;q=0.8,lt;q=0.7,ja-JP;q=0.6,ja;q=0.5
Connection: keep-alive
Host: www.<my-domain>.com
Pragma: no-cache
Referer: https://www.<my-domain>.com/
Sec-Ch-Ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Sec-Fetch-Dest: image
Sec-Fetch-Mode: no-cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
Cache-Control: public, max-age=60
Connection: keep-alive
Content-Disposition: inline; filename=59.svg
Content-Encoding: br
Content-Length: 3606
Content-Type: image/svg+xml
Date: Wed, 01 Jan 2025 16:14:08 GMT
ETag: "1735522441.7048287-18829-3530625642:br"
Last-Modified: Mon, 30 Dec 2024 01:34:01 GMT
Server: nginx/1.22.1
Strict-Transport-Security: max-age=63072000
Vary: Accept-Encoding
And finally, running curl -I https://www.<my-domain>.com/images/products/59.svg
via command line returns the following:
HTTP/1.1 200 OK
Server: nginx/1.22.1
Date: Wed, 01 Jan 2025 16:26:15 GMT
Content-Type: image/svg+xml
Content-Length: 18829
Connection: keep-alive
Content-Disposition: inline; filename=59.svg
Last-Modified: Mon, 30 Dec 2024 01:34:01 GMT
Cache-Control: public, max-age=60
ETag: "1735522441.7048287-18829-3530625642"
Strict-Transport-Security: max-age=63072000
Vary: Accept-Encoding
EDIT: I changed configuration on Nginx to use http2, but this still has no impact. Another note, when I remove compression (and thus ETags are correct without anything appended), it seems like Safari still doesn't recognize the svgs are the same and it doesn't even get a 304, but rather it refetches all the svgs.
After fighting this for a while, I found a solution to my problem. My website has many SVGs, and these SVGs use a custom font that I serve. When I serve these SVGs in an <img>
tag, they can't access my custom font (unless I include it in the nested style, which would increase the file size too much for my liking). In order to get around this, I redownloaded each SVG again on DOMContentLoaded
, and I replaced the <img>
tags with inline versions of the SVGs:
function fetchSVG(svgUrl, resultFunction = null) {
return fetch(svgUrl)
.then(response => response.ok ? response.text() : null)
.then(svgContent => {
if (svgContent === null) {
return;
}
const svgElement = new DOMParser().parseFromString(svgContent, "image/svg+xml").documentElement;
svgElement.classList.add("swapped-svg");
if (resultFunction !== null) {
resultFunction(svgElement);
}
})
.catch(error => console.error("Error loading SVG:", error));
}
document.addEventListener("DOMContentLoaded", function() {
function replaceImageWithSVG(imgElement) {
fetchSVG(imgElement.src, function(svgElement) {
imgElement.replaceWith(svgElement);
});
}
document.querySelectorAll(".svg-swap").forEach(svgSwap => {
svgSwap.addEventListener("load", function() {
replaceImageWithSVG(svgSwap);
});
if (svgSwap.complete) {
replaceImageWithSVG(svgSwap);
}
});
});
However, both the original downloads via <img>
tags AND subsequenet downloads via JavaScript all ignored the cache. If I added a delay to the downloading in JavaScript, it would use the cache for these downloads, but <img>
tags still ignored the cache in subsequent refreshes of the page. After disabling the functionality in JavaScript, I saw that caching was working again for <img>
tags. I don't know the precise reason why, but fetching via JavaScript right away after the images loading via the <img>
tag somehow messed with the cache. I found two solutions:
<img>
tags for the SVGs initially and I just
downloaded them via JavaScript on DOMContentLoaded
, the cache would work across refreshes of the website.loading="lazy"
in the <img>
tags, it somehow didn't cause this strange behavior and the cache would still work across refreshes of the website. For those that have the same issue, keep in mind you may not want this lazy loading behavior, so this might not work for you.As I mentioned, I don't know why "eager" downloading of the SVGs immediately with <img>
tags without the attribute and then pulling the SVGs via JavaScript messed with the cache, but lazy loading resolved the issue, so I went with option #2.
Either way, both of my solutions end up doing some sort of lazy loading. If I didn't want this, I think I would have been forced to include font styling inside the SVGs, or I would have had to switch the format of the images from SVG to something else.
Lastly, I would like to confirm that I only originally experienced this issue in Safari on MacOS and all browsers on iOS, and the above two solutions solved the issue.