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> 
Referer: https://www.<my-domain>/ 
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> 
Pragma: no-cache 
Referer: https://www.<my-domain>/ 
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>/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.
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.
