fix: compute IsTruncated from the absolute index in ListMultipartUploads

With an upload-id-marker the lister ranges over Uploads[startIndex:],
but passed the index of that subslice to hasMoreAfter, which indexes
the full list. A page that ended on the last remaining upload was still
reported as truncated, with next markers pointing at that upload, so a
client had to make one more request to get an empty page.

Iterate over the full list from startIndex, so hasMoreAfter gets the
real position of the next upload.
This commit is contained in:
dualfroz committed 2026-10-05 20:25:15 +02:00
1 parent a4aa4bb8d2
commit 2f2ed7ae25
3 files changed
+58 -1

No files matched your search

+54
View File
@@ -537,6 +537,60 @@ func ListMultipartUploads_delimiter_truncated(s *S3Conf) error {
})
}
func ListMultipartUploads_upload_id_marker_truncated(s *S3Conf) error {
testName := "ListMultipartUploads_upload_id_marker_truncated"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
uploads := make([]types.MultipartUpload, 0, 3)
for _, key := range []string{"bar", "baz", "foo"} {
out, err := createMp(s3client, bucket, key)
if err != nil {
return err
}
uploads = append(uploads, types.MultipartUpload{
Key: out.Key,
UploadId: out.UploadId,
StorageClass: types.StorageClassStandard,
})
}
// the listing starts after the first upload, so a page of 1
// leaves one upload behind and a page of 2 reaches the end
for _, test := range []struct {
maxUploads int32
expected []types.MultipartUpload
truncated bool
}{
{1, uploads[1:2], true},
{2, uploads[1:], false},
} {
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.ListMultipartUploads(ctx, &s3.ListMultipartUploadsInput{
Bucket: &bucket,
KeyMarker: getPtr("ba"),
UploadIdMarker: uploads[0].UploadId,
MaxUploads: &test.maxUploads,
})
cancel()
if err != nil {
return err
}
if !compareMultipartUploads(test.expected, out.Uploads) {
return fmt.Errorf("expected the mp list to be %v, instead got %v", test.expected, out.Uploads)
}
if out.IsTruncated == nil {
return fmt.Errorf("unexpected nil is-truncated")
}
if *out.IsTruncated != test.truncated {
return fmt.Errorf("expected is-truncated to be %v with max-uploads %v, instead got %v",
test.truncated, test.maxUploads, *out.IsTruncated)
}
}
return nil
})
}
func ListMultipartUploads_prefix(s *S3Conf) error {
testName := "ListMultipartUploads_prefix"
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
+2
View File
@@ -553,6 +553,7 @@ func TestListMultipartUploads(ts *TestState) {
ts.Run(ListMultipartUploads_keyMarker_not_from_list)
ts.Run(ListMultipartUploads_upload_id_marker_with_key_marker)
ts.Run(ListMultipartUploads_delimiter_truncated)
ts.Run(ListMultipartUploads_upload_id_marker_truncated)
ts.Run(ListMultipartUploads_prefix)
ts.Run(ListMultipartUploads_both_delimiter_and_prefix)
ts.Run(ListMultipartUploads_delimiter_no_matches)
@@ -3293,6 +3294,7 @@ func GetIntTests() IntTests {
"ListMultipartUploads_keyMarker_not_from_list": ListMultipartUploads_keyMarker_not_from_list,
"ListMultipartUploads_upload_id_marker_with_key_marker": ListMultipartUploads_upload_id_marker_with_key_marker,
"ListMultipartUploads_delimiter_truncated": ListMultipartUploads_delimiter_truncated,
"ListMultipartUploads_upload_id_marker_truncated": ListMultipartUploads_upload_id_marker_truncated,
"ListMultipartUploads_prefix": ListMultipartUploads_prefix,
"ListMultipartUploads_both_delimiter_and_prefix": ListMultipartUploads_both_delimiter_and_prefix,
"ListMultipartUploads_with_upload_id": ListMultipartUploads_with_upload_id,