Repository: localstack/localstack

Issue:
bug: Describe Subnets Response Diverges from AWS
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

In AWS, DescribeSubnet Response returns:
```
<?xml version="1.0" encoding="UTF-8"?>\n
    <Response>
        <Errors>
            <Error>
                <Code>InvalidSubnetID.NotFound</Code>
                <Message>The subnet ID \'test\' does not exist</Message>
            </Error>
        </Errors>
    <RequestID>f6d041c1-1c96-4720-af30-77745504e2ba</RequestID>
</Response>
```

whereas localstack returns:
```
<?xml version=\'1.0\' encoding=\'utf-8\'?>\n<Errors xmlns="http://ec2.amazonaws.com/doc/2016-11-15">
    <Error>
        <Code>InvalidSubnetID.NotFound</Code>
        <Message>aaThe subnet ID \'vpc-test\' does not exist</Message>
    </Error>
    <RequestID>EADJHWMM796CI5FT10XGFMGOXIPCBAHBWINMV0HR6SKBRODGCU5J</RequestID>
</Errors>

```

### Expected Behavior

It should return correct response structure. It causes terraform to complain.

### How are you starting LocalStack?

Custom (please describe below)

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

    make start

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

     awslocal ec2 describe-subnets --subnet-ids vpc-test --debug


### Environment

```markdown
- OS: M1 MacOS
- LocalStack: 1.0.2.dev
```


### Anything else?

Probably `call_moto` function in `localstack/services/moto.py` is changing the error code structure.


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 [developer guide](https://docs.localstack.cloud/developer-guide/).

Resolving patch:
diff --git a/localstack/aws/client.py b/localstack/aws/client.py
--- a/localstack/aws/client.py
+++ b/localstack/aws/client.py
@@ -175,7 +175,7 @@ def parse_service_exception(
     response: Response, parsed_response: Dict
 ) -> Optional[ServiceException]:
     """
-    Creates a ServiceException from a parsed response (one that botocore would return).
+    Creates a ServiceException (one ASF can handle) from a parsed response (one that botocore would return).
     It does not automatically raise the exception (see #raise_service_exception).
     :param response: Un-parsed response
     :param parsed_response: Parsed response
@@ -192,7 +192,9 @@ def parse_service_exception(
     )
     # Add all additional fields in the parsed response as members of the exception
     for key, value in parsed_response.items():
-        if key.lower() not in ["code", "message", "type"] and not hasattr(service_exception, key):
+        if key.lower() not in ["code", "message", "type", "error"] and not hasattr(
+            service_exception, key
+        ):
             setattr(service_exception, key, value)
     return service_exception
 
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
@@ -1029,8 +1029,9 @@ def _serialize_error(
             if "xmlNamespace" in operation_model.metadata
             else None
         )
-        root = ETree.Element("Errors", attr)
-        error_tag = ETree.SubElement(root, "Error")
+        root = ETree.Element("Response", attr)
+        errors_tag = ETree.SubElement(root, "Errors")
+        error_tag = ETree.SubElement(errors_tag, "Error")
         self._add_error_tags(error, error_tag)
         request_id = ETree.SubElement(root, "RequestID")
         request_id.text = gen_amzn_requestid_long()


Test patch:
diff --git a/tests/unit/aws/protocol/test_serializer.py b/tests/unit/aws/protocol/test_serializer.py
--- a/tests/unit/aws/protocol/test_serializer.py
+++ b/tests/unit/aws/protocol/test_serializer.py
@@ -3,6 +3,7 @@
 import re
 from datetime import datetime
 from typing import Any, Dict, Iterator, Optional
+from xml.etree import ElementTree
 
 import pytest
 from botocore.endpoint import convert_to_response_dict
@@ -1123,6 +1124,23 @@ def test_ec2_protocol_custom_error_serialization():
     )
 
 
+def test_ec2_protocol_errors_have_response_root_element():
+    exception = CommonServiceException(
+        "InvalidSubnetID.NotFound", "The subnet ID 'vpc-test' does not exist"
+    )
+    service = load_service("ec2")
+    response_serializer = create_serializer(service)
+    serialized_response = response_serializer.serialize_error_to_response(
+        exception, service.operation_model("DescribeSubnets")
+    )
+    body = serialized_response.data
+    parser = ElementTree.XMLParser(target=ElementTree.TreeBuilder())
+    parser.feed(body)
+    root = parser.close()
+    # The root tag contains a possible namespace, f.e. {http://ec2.amazonaws.com/doc/2016-11-15}Response.
+    assert re.sub(r"^{.*}", "", root.tag) == "Response"
+
+
 def test_restxml_without_output_shape():
     _botocore_serializer_integration_test("cloudfront", "DeleteDistribution", {}, status_code=204)
 
diff --git a/tests/unit/aws/test_client.py b/tests/unit/aws/test_client.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/aws/test_client.py
@@ -0,0 +1,23 @@
+from localstack.aws.api import ServiceException
+from localstack.aws.client import parse_service_exception
+from localstack.http import Response
+
+
+def test_parse_service_exception():
+    response = Response(status=400)
+    parsed_response = {
+        "Error": {
+            "Code": "InvalidSubnetID.NotFound",
+            "Message": "The subnet ID 'vpc-test' does not exist",
+        }
+    }
+    exception = parse_service_exception(response, parsed_response)
+    assert exception
+    assert isinstance(exception, ServiceException)
+    assert exception.code == "InvalidSubnetID.NotFound"
+    assert exception.message == "The subnet ID 'vpc-test' does not exist"
+    assert exception.status_code == 400
+    assert not exception.sender_fault
+    # Ensure that the parsed exception does not have the "Error" field from the botocore response dict
+    assert not hasattr(exception, "Error")
+    assert not hasattr(exception, "error")