From 44de803a386f746f527bac7e1d525ceef50cd1b9 Mon Sep 17 00:00:00 2001 From: Chapman Date: Fri, 2 Oct 2026 00:42:44 +0800 Subject: [PATCH] fix: emit NextPartNumberMarker only when ListParts is truncated ListPartsInfo.next_part_number_marker was a non-optional usize that defaulted to 0 and was only assigned when the response was truncated. The S3 serializer then emitted it unconditionally as Some(0), causing AWS SDK paginators to loop infinitely on part_number_marker=0 instead of terminating. Change the field to Option (None by default) and set it only inside the is_truncated branch. The S3 output layer now uses .and_then() so NextPartNumberMarker is absent when IsTruncated=false, matching AWS S3 behavior. Fixes #8208 --- crates/ecstore/src/set_disk/ops/multipart.rs | 2 +- crates/storage-api/src/multipart.rs | 2 +- rustfs/src/storage/s3_api/multipart.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ecstore/src/set_disk/ops/multipart.rs b/crates/ecstore/src/set_disk/ops/multipart.rs index f41bdf20b..61e62ea59 100644 --- a/crates/ecstore/src/set_disk/ops/multipart.rs +++ b/crates/ecstore/src/set_disk/ops/multipart.rs @@ -2125,7 +2125,7 @@ impl crate::storage_api_contracts::multipart::MultipartOperations for SetDisks { if object_parts.len() > ret.parts.len() { ret.is_truncated = true; - ret.next_part_number_marker = ret.parts.last().map(|v| v.part_num).unwrap_or_default(); + ret.next_part_number_marker = ret.parts.last().map(|v| v.part_num); } ensure_multipart_bucket_lifecycle_lock_held(bucket, object, opts)?; diff --git a/crates/storage-api/src/multipart.rs b/crates/storage-api/src/multipart.rs index ccf4d6468..a01edb9c5 100644 --- a/crates/storage-api/src/multipart.rs +++ b/crates/storage-api/src/multipart.rs @@ -61,7 +61,7 @@ pub struct ListPartsInfo { pub upload_id: String, pub storage_class: String, pub part_number_marker: usize, - pub next_part_number_marker: usize, + pub next_part_number_marker: Option, pub max_parts: usize, pub is_truncated: bool, pub parts: Vec, diff --git a/rustfs/src/storage/s3_api/multipart.rs b/rustfs/src/storage/s3_api/multipart.rs index 9acf31575..8a336f77a 100644 --- a/rustfs/src/storage/s3_api/multipart.rs +++ b/rustfs/src/storage/s3_api/multipart.rs @@ -71,7 +71,7 @@ pub(crate) fn build_list_parts_output(res: ListPartsInfo) -> ListPartsOutput { owner: Some(owner), initiator: Some(initiator), is_truncated: Some(res.is_truncated), - next_part_number_marker: res.next_part_number_marker.try_into().ok(), + next_part_number_marker: res.next_part_number_marker.and_then(|v| v.try_into().ok()), max_parts: res.max_parts.try_into().ok(), part_number_marker: res.part_number_marker.try_into().ok(), storage_class: Some( @@ -228,7 +228,7 @@ mod tests { upload_id: "upload-a".to_string(), storage_class: "STANDARD".to_string(), part_number_marker: 1, - next_part_number_marker: 2, + next_part_number_marker: Some(2), max_parts: 1000, is_truncated: true, parts: vec![PartInfo { @@ -374,7 +374,7 @@ mod tests { let input = ListPartsInfo { storage_class: "STANDARD_IA".to_string(), part_number_marker: usize::MAX, - next_part_number_marker: usize::MAX, + next_part_number_marker: Some(usize::MAX), max_parts: usize::MAX, parts: vec![PartInfo { part_num: usize::MAX,