fix: end GetObjectAttributes part pagination correctly

GetObjectAttributes decided truncation by comparing the last returned
part number with the part count (cmd/object-handlers.go:720), which is
only a coincidence of contiguous numbering. Sparse parts 1/3 reported a
complete page as truncated with a marker that loops, and parts 1/3/5
with max-parts=1 stopped after part 3 and silently dropped part 5. Set
IsTruncated in the break that proves an eligible part was left
unreturned, and zero NextPartNumberMarker when the listing is complete,
as ListObjectParts already does. Also reject negative x-amz-max-parts
and x-amz-part-number-marker in getAndValidateAttributesOpts with the
same API errors ListObjectParts uses, instead of answering an invalid
request with an empty parts listing; an absent or zero max-parts still
means the default page size.

Tests: TestAPIGetObjectAttributesPartsPagination (sparse 1/3/5 and
contiguous 1/2 walks on ErasureSD and Erasure),
TestGetAndValidateAttributesOptsPartsRange, and the sparse variants of
TestAPIGetObjectAttributesMultipartLogicalPartSize. Compatibility: no
field is added or removed; IsTruncated and NextPartNumberMarker change
only where they were wrong, and negative pagination values that no SDK
sends now fail fast.

Fixes pgsty/silo#115

Signed-off-by: Feng Ruohang <rh@vonng.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L7qJqWwy8oFA6aCXWRzXQe
This commit is contained in:
Feng Ruohang
2026-09-05 15:40:55 +08:00
parent b5409ca112
commit 2cbd48a3c3
4 changed files with 319 additions and 2 deletions
+18
View File
@@ -184,6 +184,16 @@ func getAndValidateAttributesOpts(ctx context.Context, w http.ResponseWriter, r
return opts, valid
}
// Reject out-of-range page sizes as ListObjectParts does, instead of
// answering an invalid request with an empty parts listing.
if opts.MaxParts < 0 {
apiErr = errorCodes.ToAPIErr(ErrInvalidMaxParts)
argumentName = strings.ToLower(xhttp.AmzMaxParts)
argumentValue = r.Header.Get(xhttp.AmzMaxParts)
valid = false
return opts, valid
}
if opts.MaxParts == 0 {
opts.MaxParts = maxPartsList
}
@@ -196,6 +206,14 @@ func getAndValidateAttributesOpts(ctx context.Context, w http.ResponseWriter, r
return opts, valid
}
if opts.PartNumberMarker < 0 {
apiErr = errorCodes.ToAPIErr(ErrInvalidPartNumberMarker)
argumentName = strings.ToLower(xhttp.AmzPartNumberMarker)
argumentValue = r.Header.Get(xhttp.AmzPartNumberMarker)
valid = false
return opts, valid
}
opts.ObjectAttributes = parseObjectAttributes(r.Header)
if len(opts.ObjectAttributes) < 1 {
apiErr = errorCodes.ToAPIErr(ErrInvalidAttributeName)