fix: return MalformedXML for a DeleteObjects request without objects

A DeleteObjects request whose Delete element names no objects was
answered with 200 and an empty DeleteResult. S3 rejects it with
MalformedXML, as the schema requires at least one Object.

Reject the empty list right after the body is parsed, next to the
1000 key limit check. Update the unit and integration tests that
expected the empty result and enable the REST test that covers it.

Fixes #2109
This commit is contained in:
dualfroz committed 2026-10-03 16:01:57 +02:00
1 parent edbb6c2d74
commit 62d77006d2
4 files changed
+20 -16

No files matched your search

+11
View File
@@ -57,6 +57,17 @@ func (c S3ApiController) DeleteObjects(ctx fiber.Ctx) (*Response, error) {
}, s3err.GetAPIError(s3err.ErrInvalidRequest)
}
// S3 requires at least one object in a DeleteObjects request and
// rejects an empty delete list as malformed.
if len(dObj.Objects) == 0 {
debuglogger.Logf("delete objects: no keys in the request")
return &Response{
MetaOpts: &MetaOptions{
BucketOwner: parsedAcl.Owner,
},
}, s3err.GetAPIError(s3err.ErrMalformedXML)
}
// S3 caps a single DeleteObjects request at 1000 keys. Reject an
// over-limit request before authorization and before anything reaches
// the backend, so a too-large batch can't be applied partially.
+7 -3
View File
@@ -241,13 +241,17 @@ func TestS3ApiController_DeleteObjects(t *testing.T) {
},
output: testOutput{
response: &Response{
Data: s3response.DeleteResult{},
MetaOpts: &MetaOptions{
BucketOwner: "root",
EventName: s3event.EventObjectRemovedDeleteObjects,
ObjectCount: 0,
},
},
err: s3err.GetAPIError(s3err.ErrMalformedXML),
},
configureMock: func(be *BackendMock) {
be.DeleteObjectsFunc = func(contextMoqParam context.Context, deleteObjectsInput *s3.DeleteObjectsInput) (s3response.DeleteResult, error) {
t.Error("backend DeleteObjects called for an empty delete list")
return s3response.DeleteResult{}, nil
}
},
},
{
+2 -10
View File
@@ -49,25 +49,17 @@ func DeleteObjects_empty_input(s *S3Conf) error {
}
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
out, err := s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
_, err = s3client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
Bucket: &bucket,
Delete: &types.Delete{
Objects: []types.ObjectIdentifier{},
},
})
cancel()
if err != nil {
if err := checkApiErr(err, s3err.GetAPIError(s3err.ErrMalformedXML)); err != nil {
return err
}
if len(out.Deleted) != 0 {
return fmt.Errorf("expected deleted object count 0, instead got %v",
len(out.Deleted))
}
if len(out.Errors) != 0 {
return fmt.Errorf("expected 0 errors, instead got %v", len(out.Errors))
}
ctx, cancel = context.WithTimeout(context.Background(), shortTimeout)
res, err := s3client.ListObjects(ctx, &s3.ListObjectsInput{
Bucket: &bucket,
-3
View File
@@ -36,9 +36,6 @@ source ./tests/setup.sh
# tags: curl, DeleteObjects, malformed-message
@test "REST - DeleteObjects - no objects added to payload" {
if [ "$DIRECT" != "true" ]; then
skip "https://github.com/versity/versitygw/issues/2109"
fi
run setup_bucket_and_file_v3 "$BUCKET_ONE_NAME"
assert_success
read -r bucket_name file_name <<< "$output"