From b8a8359980ea35dc0bc33598ac038613b4179a2e Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 17 Mar 2026 16:04:32 -0700 Subject: [PATCH] Match Go HTTP handlers: fix UploadResult fields, DiskStatus JSON, chunk manifest ETag - UploadResult.mime: add skip_serializing_if to omit empty MIME (Go uses omitempty) - UploadResult.contentMd5: only include when request provided Content-MD5 header - Content-MD5 response header: only set when request provided it - DiskStatuses: use camelCase field names (percentFree, percentUsed, diskType) to match Go's protobuf JSON marshaling - Chunk manifest: preserve needle ETag in expanded response headers --- seaweed-volume/src/server/handlers.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/seaweed-volume/src/server/handlers.rs b/seaweed-volume/src/server/handlers.rs index 6902fd565..2c7b4b2ee 100644 --- a/seaweed-volume/src/server/handlers.rs +++ b/seaweed-volume/src/server/handlers.rs @@ -1028,8 +1028,10 @@ async fn get_or_head_handler_inner( } // Chunk manifest expansion (needs full data) — after conditional checks, before response + // 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) { + if let Some(resp) = try_expand_chunk_manifest(&state, &n, &headers, &method, &path, &query, &etag) { return resp; } // If manifest expansion fails (invalid JSON etc.), fall through to raw data @@ -1798,6 +1800,7 @@ struct UploadResult { size: u32, #[serde(rename = "eTag", skip_serializing_if = "String::is_empty")] etag: String, + #[serde(skip_serializing_if = "String::is_empty")] mime: String, #[serde(rename = "contentMd5", skip_serializing_if = "Option::is_none")] content_md5: Option, @@ -2065,6 +2068,7 @@ pub async fn post_handler( // Validate Content-MD5 if provided let content_md5 = content_md5.or(parsed_content_md5); + let has_request_content_md5 = content_md5.is_some(); if let Some(ref expected_md5) = content_md5 { if expected_md5 != &original_content_md5 { return json_error_with_query( @@ -2284,12 +2288,13 @@ pub async fn post_handler( } else { // H2: Use Content-MD5 computed from original uncompressed data let content_md5_value = original_content_md5; + // Match Go: only include contentMd5 in response when request provided Content-MD5 let result = UploadResult { name: filename.clone(), size: original_data_size, // H3: use original size, not compressed etag: n.etag(), mime: mime_type.clone(), - content_md5: Some(content_md5_value.clone()), + content_md5: if has_request_content_md5 { Some(content_md5_value.clone()) } else { None }, }; let etag = n.etag(); let etag_header = if etag.starts_with('"') { @@ -2300,8 +2305,10 @@ pub async fn post_handler( let mut resp = (StatusCode::CREATED, axum::Json(result)).into_response(); resp.headers_mut() .insert(header::ETAG, etag_header.parse().unwrap()); - resp.headers_mut() - .insert("Content-MD5", content_md5_value.parse().unwrap()); + if has_request_content_md5 { + resp.headers_mut() + .insert("Content-MD5", content_md5_value.parse().unwrap()); + } resp } } @@ -2879,6 +2886,7 @@ fn try_expand_chunk_manifest( method: &Method, path: &str, query: &ReadQueryParams, + etag: &str, ) -> Option { let data = if n.is_compressed() { use flate2::read::GzDecoder; @@ -2992,6 +3000,10 @@ fn try_expand_chunk_manifest( }; let mut response_headers = HeaderMap::new(); + // Preserve ETag from the needle (matches Go: ETag is set before tryHandleChunkedFile) + if let Ok(etag_val) = etag.parse() { + response_headers.insert(header::ETAG, etag_val); + } response_headers.insert(header::CONTENT_TYPE, content_type.parse().unwrap()); response_headers.insert("X-File-Store", "chunked".parse().unwrap()); response_headers.insert(header::ACCEPT_RANGES, "bytes".parse().unwrap()); @@ -3055,14 +3067,15 @@ fn build_disk_statuses(store: &crate::storage::store::Store) -> Vec