From ce1a40f8729a9a3116320bd2ed8d73f24061caa9 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 17 Mar 2026 22:46:50 -0700 Subject: [PATCH] Add Last-Modified, pairs, and S3 headers to chunk manifest responses Go sets Last-Modified, needle pairs, and S3 pass-through headers on the response writer BEFORE calling tryHandleChunkedFile. Since the Rust chunk manifest handler created fresh response headers and returned early, these headers were missing from chunk manifest responses. Now passes last_modified_str into the chunk manifest handler and applies pairs and S3 pass-through query params (response-cache-control, response-content-encoding, etc.) to the chunk manifest response headers. --- seaweed-volume/src/server/handlers.rs | 53 ++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/seaweed-volume/src/server/handlers.rs b/seaweed-volume/src/server/handlers.rs index e4d849bf4..3d5ba372b 100644 --- a/seaweed-volume/src/server/handlers.rs +++ b/seaweed-volume/src/server/handlers.rs @@ -1037,7 +1037,7 @@ async fn get_or_head_handler_inner( // Pass ETag so chunk manifest responses include it (matches Go: ETag is set on the // response writer before tryHandleChunkedFile runs). if n.is_chunk_manifest() && !bypass_cm { - if let Some(resp) = try_expand_chunk_manifest(&state, &n, &headers, &method, &path, &query, &etag) { + if let Some(resp) = try_expand_chunk_manifest(&state, &n, &headers, &method, &path, &query, &etag, &last_modified_str) { return resp; } // If manifest expansion fails (invalid JSON etc.), fall through to raw data @@ -2936,6 +2936,7 @@ fn try_expand_chunk_manifest( path: &str, query: &ReadQueryParams, etag: &str, + last_modified_str: &Option, ) -> Option { let data = if n.is_compressed() { use flate2::read::GzDecoder; @@ -3055,6 +3056,56 @@ fn try_expand_chunk_manifest( response_headers.insert("X-File-Store", "chunked".parse().unwrap()); response_headers.insert(header::ACCEPT_RANGES, "bytes".parse().unwrap()); + // Last-Modified — Go sets this on the response writer before tryHandleChunkedFile + if let Some(ref lm) = last_modified_str { + if let Ok(hval) = lm.parse() { + response_headers.insert(header::LAST_MODIFIED, hval); + } + } + + // Pairs — Go sets needle pairs on the response writer before tryHandleChunkedFile + if n.has_pairs() && !n.pairs.is_empty() { + if let Ok(pair_map) = + serde_json::from_slice::>(&n.pairs) + { + for (k, v) in &pair_map { + if let (Ok(hname), Ok(hval)) = ( + axum::http::HeaderName::from_bytes(k.as_bytes()), + axum::http::HeaderValue::from_str(v), + ) { + response_headers.insert(hname, hval); + } + } + } + } + + // S3 response passthrough headers — Go sets these via AdjustPassthroughHeaders + if let Some(ref cc) = query.response_cache_control { + if let Ok(hval) = cc.parse() { + response_headers.insert(header::CACHE_CONTROL, hval); + } + } + if let Some(ref ce) = query.response_content_encoding { + if let Ok(hval) = ce.parse() { + response_headers.insert(header::CONTENT_ENCODING, hval); + } + } + if let Some(ref exp) = query.response_expires { + if let Ok(hval) = exp.parse() { + response_headers.insert(header::EXPIRES, hval); + } + } + if let Some(ref cl) = query.response_content_language { + if let Ok(hval) = cl.parse() { + response_headers.insert("Content-Language", hval); + } + } + if let Some(ref cd) = query.response_content_disposition { + if let Ok(hval) = cd.parse() { + response_headers.insert(header::CONTENT_DISPOSITION, hval); + } + } + // Content-Disposition if !filename.is_empty() { let disposition_type = if let Some(ref dl_val) = query.dl {