From 8d6711dcdb927f45f60c6b3b71445dc166e54013 Mon Sep 17 00:00:00 2001 From: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Date: Tue, 12 May 2026 13:50:32 -0400 Subject: [PATCH] fix(desktop): forward cache headers in media proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WKWebView fetches images via the localhost media proxy and the sprout-media:// protocol handler. Both paths only forwarded a hard-coded allowlist of headers (content-type, content-range, accept-ranges, content-length) and stripped everything else — including `Cache-Control`, which the relay already sets to `public, max-age=31536000, immutable` for content-addressed media. Without those headers WKWebView treats every response as uncacheable and re-fetches every image on each channel switch. Forward `cache-control`, `etag`, and `last-modified` in both proxy paths. Media URLs are sha256-addressed, so the upstream's immutable caching directive is safe to honor verbatim. `etag`/`last-modified` are forwarded too in case the relay ever starts sending them. Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> --- desktop/src-tauri/src/media_proxy.rs | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/desktop/src-tauri/src/media_proxy.rs b/desktop/src-tauri/src/media_proxy.rs index 75402dc45..e154e9c49 100644 --- a/desktop/src-tauri/src/media_proxy.rs +++ b/desktop/src-tauri/src/media_proxy.rs @@ -78,6 +78,15 @@ async fn proxy_handler(AxumState(state): AxumState, req: Request) -> "content-range", "accept-ranges", "content-length", + // Cache-related headers — let WKWebView's HTTP cache do its job so + // images don't re-fetch on every channel switch. Media URLs are + // content-addressed (sha256 in path), so the relay sends + // `Cache-Control: public, max-age=31536000, immutable` and we + // forward it verbatim. `etag`/`last-modified` are forwarded for + // future-proofing if upstream ever adds them. + "cache-control", + "etag", + "last-modified", ] { if let Some(val) = resp.headers().get(*key) { if let Ok(v) = HeaderValue::from_bytes(val.as_bytes()) { @@ -205,6 +214,27 @@ pub async fn handle_sprout_media( .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); + // Propagate cache-related headers so WKWebView's HTTP cache + // can avoid re-fetching content-addressed media on every + // channel switch. The relay sends + // `Cache-Control: public, max-age=31536000, immutable`; + // `etag`/`last-modified` are forwarded if upstream supplies them. + let cache_control = resp + .headers() + .get("cache-control") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + let etag = resp + .headers() + .get("etag") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + let last_modified = resp + .headers() + .get("last-modified") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + // OOM guard: if this is a non-range GET and the upstream body is // larger than our cap, bail with 413 instead of buffering into RAM. // Tauri's protocol handler requires Vec so we can't truly stream. @@ -235,6 +265,15 @@ pub async fn handle_sprout_media( if let Some(ref cl) = content_length { builder = builder.header("content-length", cl); } + if let Some(ref cc) = cache_control { + builder = builder.header("cache-control", cc); + } + if let Some(ref e) = etag { + builder = builder.header("etag", e); + } + if let Some(ref lm) = last_modified { + builder = builder.header("last-modified", lm); + } builder .body(bytes.to_vec()) .unwrap_or_else(|_| error_response(500, "response build failed"))