Repository: localstack/localstack

Issue:
bug: CreateMultipartUpload should return InitiateMultipartUploadResult element
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

Initiating a multipart S3 upload to LocalStack returns an XML document with a root element CreateMultipartUploadOutput. This causes an error in the ex_aws_s3 library I'm using. We've used the combination of LocalStack + ex_aws_s3 successfully for a few years and the error started occurring a few days ago.

### Expected Behavior

The element should be called InitiateMultipartUploadResult.

### How are you starting LocalStack?

With a `docker run` command

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

    docker run -p 4566:4566 localstack/localstack

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

    aws --debug --endpoint-url=http://localhost:4566 s3api create-multipart-upload --bucket my-bucket --key 'multipart/01'


### Environment

```markdown
- OS: Ubuntu 20.04.5 LTS
- LocalStack: latest
```


### Anything else?

S3 docs specifying InitiateMultipartUploadResult:
https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html

Reference in official AWS Java client:
https://github.com/aws/aws-sdk-java/blob/ef22b94750d4c155d74b09012b3be5d9d98bbd33/aws-java-sdk-s3/src/main/java/com/amazonaws/services/s3/model/transform/XmlResponsesSaxParser.java#L1865

Where our particular error gets raised in ex_aws_s3:
https://github.com/ex-aws/ex_aws_s3/blob/7e12f2b0578b2620c047d2e44c40a6de8773584a/lib/ex_aws/s3/parsers.ex#L78


Discussion:
Welcome to LocalStack! Thanks for reporting your first issue and our team will be working towards fixing the issue for you or reach out for more background information. We recommend joining our [Slack Community](https://localstack.cloud/contact/) for real-time help and drop a message to LocalStack Pro Support if you are a Pro user! If you are willing to contribute towards fixing this issue, please have a look at our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [contributing guide](https://docs.localstack.cloud/contributing/).
Hi @pguillory and thanks for your report! I've answered in your PR as well: https://github.com/localstack/localstack/pull/8032#issuecomment-1492774815

We are currently taking a look at all the operations returning a root XML node different that was is specified in the specs, and will try to correct most of them quickly. Thanks again for your report! 

Resolving patch:
diff --git a/localstack/aws/api/s3/__init__.py b/localstack/aws/api/s3/__init__.py
--- a/localstack/aws/api/s3/__init__.py
+++ b/localstack/aws/api/s3/__init__.py
@@ -1372,6 +1372,32 @@ class DeleteObjectTaggingRequest(ServiceRequest):
     ExpectedBucketOwner: Optional[AccountId]
 
 
+class Error(TypedDict, total=False):
+    Key: Optional[ObjectKey]
+    VersionId: Optional[ObjectVersionId]
+    Code: Optional[Code]
+    Message: Optional[Message]
+
+
+Errors = List[Error]
+
+
+class DeletedObject(TypedDict, total=False):
+    Key: Optional[ObjectKey]
+    VersionId: Optional[ObjectVersionId]
+    DeleteMarker: Optional[DeleteMarker]
+    DeleteMarkerVersionId: Optional[DeleteMarkerVersionId]
+
+
+DeletedObjects = List[DeletedObject]
+
+
+class DeleteObjectsOutput(TypedDict, total=False):
+    Deleted: Optional[DeletedObjects]
+    RequestCharged: Optional[RequestCharged]
+    Errors: Optional[Errors]
+
+
 class DeleteObjectsRequest(ServiceRequest):
     Bucket: BucketName
     Delete: Delete
@@ -1387,16 +1413,6 @@ class DeletePublicAccessBlockRequest(ServiceRequest):
     ExpectedBucketOwner: Optional[AccountId]
 
 
-class DeletedObject(TypedDict, total=False):
-    Key: Optional[ObjectKey]
-    VersionId: Optional[ObjectVersionId]
-    DeleteMarker: Optional[DeleteMarker]
-    DeleteMarkerVersionId: Optional[DeleteMarkerVersionId]
-
-
-DeletedObjects = List[DeletedObject]
-
-
 class ReplicationTimeValue(TypedDict, total=False):
     Minutes: Optional[Minutes]
 
@@ -1438,20 +1454,10 @@ class EndEvent(TypedDict, total=False):
     pass
 
 
-class Error(TypedDict, total=False):
-    Key: Optional[ObjectKey]
-    VersionId: Optional[ObjectVersionId]
-    Code: Optional[Code]
-    Message: Optional[Message]
-
-
 class ErrorDocument(TypedDict, total=False):
     Key: ObjectKey
 
 
-Errors = List[Error]
-
-
 class EventBridgeConfiguration(TypedDict, total=False):
     pass
 
@@ -2262,6 +2268,39 @@ class ListBucketMetricsConfigurationsRequest(ServiceRequest):
     ExpectedBucketOwner: Optional[AccountId]
 
 
+class ListBucketsOutput(TypedDict, total=False):
+    Buckets: Optional[Buckets]
+    Owner: Optional[Owner]
+
+
+class MultipartUpload(TypedDict, total=False):
+    UploadId: Optional[MultipartUploadId]
+    Key: Optional[ObjectKey]
+    Initiated: Optional[Initiated]
+    StorageClass: Optional[StorageClass]
+    Owner: Optional[Owner]
+    Initiator: Optional[Initiator]
+    ChecksumAlgorithm: Optional[ChecksumAlgorithm]
+
+
+MultipartUploadList = List[MultipartUpload]
+
+
+class ListMultipartUploadsOutput(TypedDict, total=False):
+    Bucket: Optional[BucketName]
+    KeyMarker: Optional[KeyMarker]
+    UploadIdMarker: Optional[UploadIdMarker]
+    NextKeyMarker: Optional[NextKeyMarker]
+    Prefix: Optional[Prefix]
+    Delimiter: Optional[Delimiter]
+    NextUploadIdMarker: Optional[NextUploadIdMarker]
+    MaxUploads: Optional[MaxUploads]
+    IsTruncated: Optional[IsTruncated]
+    Uploads: Optional[MultipartUploadList]
+    CommonPrefixes: Optional[CommonPrefixList]
+    EncodingType: Optional[EncodingType]
+
+
 class ListMultipartUploadsRequest(ServiceRequest):
     Bucket: BucketName
     Delimiter: Optional[Delimiter]
@@ -2315,6 +2354,33 @@ class ListObjectVersionsRequest(ServiceRequest):
     ExpectedBucketOwner: Optional[AccountId]
 
 
+class Object(TypedDict, total=False):
+    Key: Optional[ObjectKey]
+    LastModified: Optional[LastModified]
+    ETag: Optional[ETag]
+    ChecksumAlgorithm: Optional[ChecksumAlgorithmList]
+    Size: Optional[Size]
+    StorageClass: Optional[ObjectStorageClass]
+    Owner: Optional[Owner]
+
+
+ObjectList = List[Object]
+
+
+class ListObjectsOutput(TypedDict, total=False):
+    IsTruncated: Optional[IsTruncated]
+    Marker: Optional[Marker]
+    NextMarker: Optional[NextMarker]
+    Contents: Optional[ObjectList]
+    Name: Optional[BucketName]
+    Prefix: Optional[Prefix]
+    Delimiter: Optional[Delimiter]
+    MaxKeys: Optional[MaxKeys]
+    CommonPrefixes: Optional[CommonPrefixList]
+    EncodingType: Optional[EncodingType]
+    BucketRegion: Optional[BucketRegion]
+
+
 class ListObjectsRequest(ServiceRequest):
     Bucket: BucketName
     Delimiter: Optional[Delimiter]
@@ -2326,6 +2392,22 @@ class ListObjectsRequest(ServiceRequest):
     ExpectedBucketOwner: Optional[AccountId]
 
 
+class ListObjectsV2Output(TypedDict, total=False):
+    IsTruncated: Optional[IsTruncated]
+    Contents: Optional[ObjectList]
+    Name: Optional[BucketName]
+    Prefix: Optional[Prefix]
+    Delimiter: Optional[Delimiter]
+    MaxKeys: Optional[MaxKeys]
+    CommonPrefixes: Optional[CommonPrefixList]
+    EncodingType: Optional[EncodingType]
+    KeyCount: Optional[KeyCount]
+    ContinuationToken: Optional[Token]
+    NextContinuationToken: Optional[NextToken]
+    StartAfter: Optional[StartAfter]
+    BucketRegion: Optional[BucketRegion]
+
+
 class ListObjectsV2Request(ServiceRequest):
     Bucket: BucketName
     Delimiter: Optional[Delimiter]
@@ -2389,19 +2471,6 @@ class MetadataEntry(TypedDict, total=False):
     Value: Optional[MetadataValue]
 
 
-class MultipartUpload(TypedDict, total=False):
-    UploadId: Optional[MultipartUploadId]
-    Key: Optional[ObjectKey]
-    Initiated: Optional[Initiated]
-    StorageClass: Optional[StorageClass]
-    Owner: Optional[Owner]
-    Initiator: Optional[Initiator]
-    ChecksumAlgorithm: Optional[ChecksumAlgorithm]
-
-
-MultipartUploadList = List[MultipartUpload]
-
-
 class QueueConfiguration(TypedDict, total=False):
     Id: Optional[NotificationId]
     QueueArn: QueueArn
@@ -2449,17 +2518,6 @@ class NotificationConfigurationDeprecated(TypedDict, total=False):
     CloudFunctionConfiguration: Optional[CloudFunctionConfiguration]
 
 
-class Object(TypedDict, total=False):
-    Key: Optional[ObjectKey]
-    LastModified: Optional[LastModified]
-    ETag: Optional[ETag]
-    ChecksumAlgorithm: Optional[ChecksumAlgorithmList]
-    Size: Optional[Size]
-    StorageClass: Optional[ObjectStorageClass]
-    Owner: Optional[Owner]
-
-
-ObjectList = List[Object]
 UserMetadata = List[MetadataEntry]
 
 
@@ -3020,12 +3078,6 @@ class HeadBucketOutput(TypedDict, total=False):
     BucketContentType: Optional[BucketContentType]
 
 
-class DeleteResult(TypedDict, total=False):
-    Deleted: Optional[DeletedObjects]
-    RequestCharged: Optional[RequestCharged]
-    Errors: Optional[Errors]
-
-
 class PostObjectRequest(ServiceRequest):
     Body: Optional[IO[Body]]
     Bucket: BucketName
@@ -3054,44 +3106,6 @@ class PostResponse(TypedDict, total=False):
     RequestCharged: Optional[RequestCharged]
 
 
-class ListAllMyBucketsResult(TypedDict, total=False):
-    Buckets: Optional[Buckets]
-    Owner: Optional[Owner]
-
-
-class ListBucketResult(TypedDict, total=False):
-    IsTruncated: Optional[IsTruncated]
-    Contents: Optional[ObjectList]
-    Name: Optional[BucketName]
-    Prefix: Optional[Prefix]
-    Delimiter: Optional[Delimiter]
-    MaxKeys: Optional[MaxKeys]
-    CommonPrefixes: Optional[CommonPrefixList]
-    EncodingType: Optional[EncodingType]
-    KeyCount: Optional[KeyCount]
-    ContinuationToken: Optional[Token]
-    NextContinuationToken: Optional[NextToken]
-    StartAfter: Optional[StartAfter]
-    Marker: Optional[Marker]
-    NextMarker: Optional[NextMarker]
-    BucketRegion: Optional[BucketRegion]
-
-
-class ListMultipartUploadsResult(TypedDict, total=False):
-    Bucket: Optional[BucketName]
-    KeyMarker: Optional[KeyMarker]
-    UploadIdMarker: Optional[UploadIdMarker]
-    NextKeyMarker: Optional[NextKeyMarker]
-    Prefix: Optional[Prefix]
-    Delimiter: Optional[Delimiter]
-    NextUploadIdMarker: Optional[NextUploadIdMarker]
-    MaxUploads: Optional[MaxUploads]
-    IsTruncated: Optional[IsTruncated]
-    Uploads: Optional[MultipartUploadList]
-    CommonPrefixes: Optional[CommonPrefixList]
-    EncodingType: Optional[EncodingType]
-
-
 class S3Api:
 
     service = "s3"
@@ -3357,7 +3371,7 @@ def delete_objects(
         bypass_governance_retention: BypassGovernanceRetention = None,
         expected_bucket_owner: AccountId = None,
         checksum_algorithm: ChecksumAlgorithm = None,
-    ) -> DeleteResult:
+    ) -> DeleteObjectsOutput:
         raise NotImplementedError
 
     @handler("DeletePublicAccessBlock")
@@ -3495,7 +3509,7 @@ def get_bucket_request_payment(
     @handler("GetBucketTagging")
     def get_bucket_tagging(
         self, context: RequestContext, bucket: BucketName, expected_bucket_owner: AccountId = None
-    ) -> Tagging:
+    ) -> GetBucketTaggingOutput:
         raise NotImplementedError
 
     @handler("GetBucketVersioning")
@@ -3607,7 +3621,7 @@ def get_object_tagging(
         version_id: ObjectVersionId = None,
         expected_bucket_owner: AccountId = None,
         request_payer: RequestPayer = None,
-    ) -> Tagging:
+    ) -> GetObjectTaggingOutput:
         raise NotImplementedError
 
     @handler("GetObjectTorrent")
@@ -3695,7 +3709,7 @@ def list_bucket_metrics_configurations(
     def list_buckets(
         self,
         context: RequestContext,
-    ) -> ListAllMyBucketsResult:
+    ) -> ListBucketsOutput:
         raise NotImplementedError
 
     @handler("ListMultipartUploads")
@@ -3710,7 +3724,7 @@ def list_multipart_uploads(
         prefix: Prefix = None,
         upload_id_marker: UploadIdMarker = None,
         expected_bucket_owner: AccountId = None,
-    ) -> ListMultipartUploadsResult:
+    ) -> ListMultipartUploadsOutput:
         raise NotImplementedError
 
     @handler("ListObjectVersions")
@@ -3740,7 +3754,7 @@ def list_objects(
         prefix: Prefix = None,
         request_payer: RequestPayer = None,
         expected_bucket_owner: AccountId = None,
-    ) -> ListBucketResult:
+    ) -> ListObjectsOutput:
         raise NotImplementedError
 
     @handler("ListObjectsV2")
@@ -3757,7 +3771,7 @@ def list_objects_v2(
         start_after: StartAfter = None,
         request_payer: RequestPayer = None,
         expected_bucket_owner: AccountId = None,
-    ) -> ListBucketResult:
+    ) -> ListObjectsV2Output:
         raise NotImplementedError
 
     @handler("ListParts")
diff --git a/localstack/aws/protocol/serializer.py b/localstack/aws/protocol/serializer.py
--- a/localstack/aws/protocol/serializer.py
+++ b/localstack/aws/protocol/serializer.py
@@ -1362,10 +1362,51 @@ def _serialize_content_type(
 class S3ResponseSerializer(RestXMLResponseSerializer):
     """
     The ``S3ResponseSerializer`` adds some minor logic to handle S3 specific peculiarities with the error response
-    serialization.
+    serialization and the root node tag.
     """
 
     SUPPORTED_MIME_TYPES = [APPLICATION_XML, TEXT_XML]
+    _RESPONSE_ROOT_TAGS = {
+        "CompleteMultipartUploadOutput": "CompleteMultipartUploadResult",
+        "CopyObjectOutput": "CopyObjectResult",
+        "CreateMultipartUploadOutput": "InitiateMultipartUploadResult",
+        "DeleteObjectsOutput": "DeleteResult",
+        "GetBucketAccelerateConfigurationOutput": "AccelerateConfiguration",
+        "GetBucketAclOutput": "AccessControlPolicy",
+        "GetBucketAnalyticsConfigurationOutput": "AnalyticsConfiguration",
+        "GetBucketCorsOutput": "CORSConfiguration",
+        "GetBucketEncryptionOutput": "ServerSideEncryptionConfiguration",
+        "GetBucketIntelligentTieringConfigurationOutput": "IntelligentTieringConfiguration",
+        "GetBucketInventoryConfigurationOutput": "InventoryConfiguration",
+        "GetBucketLifecycleOutput": "LifecycleConfiguration",
+        "GetBucketLifecycleConfigurationOutput": "LifecycleConfiguration",
+        "GetBucketLoggingOutput": "BucketLoggingStatus",
+        "GetBucketMetricsConfigurationOutput": "MetricsConfiguration",
+        "NotificationConfigurationDeprecated": "NotificationConfiguration",
+        "GetBucketOwnershipControlsOutput": "OwnershipControls",
+        "GetBucketPolicyStatusOutput": "PolicyStatus",
+        "GetBucketReplicationOutput": "ReplicationConfiguration",
+        "GetBucketRequestPaymentOutput": "RequestPaymentConfiguration",
+        "GetBucketTaggingOutput": "Tagging",
+        "GetBucketVersioningOutput": "VersioningConfiguration",
+        "GetBucketWebsiteOutput": "WebsiteConfiguration",
+        "GetObjectAclOutput": "AccessControlPolicy",
+        "GetObjectLegalHoldOutput": "LegalHold",
+        "GetObjectLockConfigurationOutput": "ObjectLockConfiguration",
+        "GetObjectRetentionOutput": "Retention",
+        "GetObjectTaggingOutput": "Tagging",
+        "GetPublicAccessBlockOutput": "PublicAccessBlockConfiguration",
+        "ListBucketAnalyticsConfigurationsOutput": "ListBucketAnalyticsConfigurationResult",
+        "ListBucketInventoryConfigurationsOutput": "ListInventoryConfigurationsResult",
+        "ListBucketMetricsConfigurationsOutput": "ListMetricsConfigurationsResult",
+        "ListBucketsOutput": "ListAllMyBucketsResult",
+        "ListMultipartUploadsOutput": "ListMultipartUploadsResult",
+        "ListObjectsOutput": "ListBucketResult",
+        "ListObjectsV2Output": "ListBucketResult",
+        "ListObjectVersionsOutput": "ListVersionsResult",
+        "ListPartsOutput": "ListPartsResult",
+        "UploadPartCopyOutput": "CopyPartResult",
+    }
 
     def _serialize_response(
         self,
@@ -1409,6 +1450,19 @@ def _serialize_error(
 
         response.set_response(self._encode_payload(self._node_to_string(root, mime_type)))
 
+    def _serialize_body_params(
+        self,
+        params: dict,
+        shape: Shape,
+        operation_model: OperationModel,
+        mime_type: str,
+    ) -> Optional[str]:
+        root = self._serialize_body_params_to_xml(params, shape, operation_model, mime_type)
+        # S3 does not follow the specs on the root tag name for 41 of 44 operations
+        root.tag = self._RESPONSE_ROOT_TAGS.get(root.tag, root.tag)
+        self._prepare_additional_traits_in_xml(root)
+        return self._node_to_string(root, mime_type)
+
     def _prepare_additional_traits_in_response(
         self, response: HttpResponse, operation_model: OperationModel
     ):
diff --git a/localstack/services/s3/provider.py b/localstack/services/s3/provider.py
--- a/localstack/services/s3/provider.py
+++ b/localstack/services/s3/provider.py
@@ -30,9 +30,9 @@
     Delete,
     DeleteObjectOutput,
     DeleteObjectRequest,
+    DeleteObjectsOutput,
     DeleteObjectTaggingOutput,
     DeleteObjectTaggingRequest,
-    DeleteResult,
     ETag,
     GetBucketAclOutput,
     GetBucketCorsOutput,
@@ -56,10 +56,11 @@
     InvalidBucketName,
     InvalidPartOrder,
     InvalidStorageClass,
-    ListBucketResult,
+    ListMultipartUploadsOutput,
     ListMultipartUploadsRequest,
-    ListMultipartUploadsResult,
+    ListObjectsOutput,
     ListObjectsRequest,
+    ListObjectsV2Output,
     ListObjectsV2Request,
     MissingSecurityHeader,
     MultipartUpload,
@@ -286,8 +287,8 @@ def list_objects(
         self,
         context: RequestContext,
         request: ListObjectsRequest,
-    ) -> ListBucketResult:
-        response: ListBucketResult = call_moto(context)
+    ) -> ListObjectsOutput:
+        response: ListObjectsOutput = call_moto(context)
 
         if "Marker" not in response:
             response["Marker"] = request.get("Marker") or ""
@@ -307,15 +308,15 @@ def list_objects(
             bucket = get_bucket_from_moto(moto_backend, bucket=request["Bucket"])
             response["BucketRegion"] = bucket.region_name
 
-        return ListBucketResult(**response)
+        return response
 
     @handler("ListObjectsV2", expand=False)
     def list_objects_v2(
         self,
         context: RequestContext,
         request: ListObjectsV2Request,
-    ) -> ListBucketResult:
-        response: ListBucketResult = call_moto(context)
+    ) -> ListObjectsV2Output:
+        response: ListObjectsV2Output = call_moto(context)
 
         encoding_type = request.get("EncodingType")
         if "EncodingType" not in response and encoding_type:
@@ -487,7 +488,7 @@ def delete_objects(
         bypass_governance_retention: BypassGovernanceRetention = None,
         expected_bucket_owner: AccountId = None,
         checksum_algorithm: ChecksumAlgorithm = None,
-    ) -> DeleteResult:
+    ) -> DeleteObjectsOutput:
         # TODO: implement DeleteMarker response
         objects: List[ObjectIdentifier] = delete.get("Objects")
         deleted_objects = {}
@@ -504,7 +505,7 @@ def delete_objects(
             )
 
             deleted_objects[key] = s3_notification_ctx
-        result: DeleteResult = call_moto(context)
+        result: DeleteObjectsOutput = call_moto(context)
         for deleted in result.get("Deleted"):
             if deleted_objects.get(deleted["Key"]):
                 self._notify(context, deleted_objects.get(deleted["Key"]))
@@ -566,7 +567,7 @@ def list_multipart_uploads(
         self,
         context: RequestContext,
         request: ListMultipartUploadsRequest,
-    ) -> ListMultipartUploadsResult:
+    ) -> ListMultipartUploadsOutput:
 
         # TODO: implement KeyMarker and UploadIdMarker (using sort)
         # implement Delimiter and MaxUploads
@@ -599,7 +600,7 @@ def list_multipart_uploads(
             for upload in multiparts
         ]
 
-        response = ListMultipartUploadsResult(
+        response = ListMultipartUploadsOutput(
             Bucket=request["Bucket"],
             MaxUploads=request.get("MaxUploads") or 1000,
             IsTruncated=False,


Test patch: