Repository: getmoto/moto
Issue: S3 DeleteObjects Operation issue in Moto results failure in LocalStack

Sending a DeleteObjects request from AWS Go SDK to localStack ( set to latest, so fetched moto-ext>=1.3.15.12) results is 500 error.


DeleteObject operation works successfully, it's the DeleteObject**s** that fails. Looking into the difference between the request payload (AWS Go SDK github.com/aws/aws-sdk-go v1.28.6):

**DeleteObject:** 
`op := &request.Operation{
		Name:       opDeleteObject,
		HTTPMethod: "DELETE",
		HTTPPath:   "/{Bucket}/{Key+}",
	}`

Where `const opDeleteObject = "DeleteObject"` 

**DeleteObjects**
`op := &request.Operation{
		Name:       opDeleteObjects,
		HTTPMethod: "POST",
		HTTPPath:   "/{Bucket}?delete",
	}`

Where `const opDeleteObjects = "DeleteObjects"`

**In `_bucket_response_delete_keys` method of `class ResponseObject` **_line 843_** the check is:
`objects = body_dict["Delete"].get("Object", [])`** 

In case of `DeleteObjects` the key is "Objects", so objects variable will be []

Note the difference between how each of these operations is called: 

**DeleteObject**

`_, err := s.Client.DeleteObject(&s3.DeleteObjectInput{Bucket: aws.String(bucket), Key: aws.String(key)})
`
**DeleteObjects**
`_, err := s.Client.DeleteObjects(&s3.DeleteObjectsInput{Bucket: aws.String(bucket), Delete: &s3.Delete{
   Objects: keys,
   Quiet: aws.Bool(false),
}})
`

the Delete struct has a field **Objects** instead of **Object**
`
	Objects []*ObjectIdentifier `locationName:"Object" type:"list" flattened:"true" required:"true"`
` 



######################################################################################


How to replicate:

Place some files in 'examplebucket'
call the DeleteObjects (code from AWS Docs) which the keys:

`svc := s3.New(session.New())
input := &s3.DeleteObjectsInput{
    Bucket: aws.String("examplebucket"),
    Delete: &s3.Delete{
        Objects: []*s3.ObjectIdentifier{
            {
                Key: aws.String("objectkey1"),
            },
            {
                Key: aws.String("objectkey2"),
            },
        },
        Quiet: aws.Bool(false),
    },
}

result, err := svc.DeleteObjects(input)
if err != nil {
    if aerr, ok := err.(awserr.Error); ok {
        switch aerr.Code() {
        default:
            fmt.Println(aerr.Error())
        }
    } else {
        // Print the error, cast err to awserr.Error to get the Code and
        // Message from an error.
        fmt.Println(err.Error())
    }
    return
}

fmt.Println(result)
}`







Changed files:
moto/s3/responses.py, tests/test_s3/test_s3.py

diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index e1ab93860..98f28f012 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -137,8 +137,13 @@ def parse_key_name(pth):
 
 
 def is_delete_keys(request, path, bucket_name):
-    return path == "/?delete" or (
-        path == "/" and getattr(request, "query_string", "") == "delete"
+    # GOlang sends a request as url/?delete= (treating it as a normal key=value, even if the value is empty)
+    # Python sends a request as url/?delete (treating it as a flag)
+    # https://github.com/spulec/moto/issues/2937
+    return (
+        path == "/?delete"
+        or path == "/?delete="
+        or (path == "/" and getattr(request, "query_string", "") == "delete")
     )
 
 
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index 86b892315..f60e0293e 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -3765,7 +3765,7 @@ def test_paths_with_leading_slashes_work():
 
 @mock_s3
 def test_root_dir_with_empty_name_works():
-    if os.environ.get("TEST_SERVER_MODE", "false").lower() == "true":
+    if settings.TEST_SERVER_MODE:
         raise SkipTest("Does not work in server mode due to error in Workzeug")
     store_and_read_back_a_key("/")