Repository: getmoto/moto
Pull request #8570 (2025-02-06 13:28:55)

EC2: Handle filtering without Values
When a filter is provided that doesn't have `Values` or has an empty list as `Values` moto raises TypeError 


```
% aws ec2 describe-subnets --filters Name=vpc-id
{
    "Subnets": []
}
```

```
% aws ec2 describe-subnets --filters Name=vpc-id --endpoint-url http://127.0.0.1:5000

Unable to parse response (syntax error: line 1, column 0), invalid XML received. Further retries may succeed:
b'<!doctype html>\n<html lang=en>\n<title>500 Internal Server Error</title>\n<h1>Internal Server Error</h1>\n<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>\n'
```

from logs:

```
  File "moto/moto/ec2/responses/_base_response.py", line 20, in <dictcomp>
    return {f["Name"]: f["Value"] for f in _filters}
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: string indices must be integers, not 'str'
```

Same for 

```
% aws ec2 describe-subnets --filters '[{"Name": "vpc-id", "Values": []}]'            
{
    "Subnets": []
}
```

```
% aws ec2 describe-subnets --filters '[{"Name": "vpc-id", "Values": []}]' --endpoint-url http://127.0.0.1:5000

Unable to parse response (syntax error: line 1, column 0), invalid XML received. Further retries may succeed:
b'<!doctype html>\n<html lang=en>\n<title>500 Internal Server Error</title>\n<h1>Internal Server Error</h1>\n<p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>\n'
```

This PR fixes this issue and also returns the same error as AWS for the case that a Name is not provided for a filter

```
 % aws ec2 describe-subnets --filters Values=123

An error occurred (InvalidParameterValue) when calling the DescribeSubnets operation: The filter 'null' is invalid
```


Tests are added for 3 cases 
* describe_images
* describe_subnets
* describe_availability_zones

but it affects more EC2 actions that share the same filtering logic


Issue was originally reported in localstack https://github.com/localstack/localstack/issues/11765


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8570?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 92.65%. Comparing base [(`98e2730`)](https://app.codecov.io/gh/getmoto/moto/commit/98e273086f475635fa77669fb4e40edcb0120431?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`da93140`)](https://app.codecov.io/gh/getmoto/moto/commit/da93140e85169aab337c0c3547342f9280fb1afd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #8570   +/-   ##
=======================================
  Coverage   92.65%   92.65%           
=======================================
  Files        1231     1231           
  Lines      106851   106857    +6     
=======================================
+ Hits        99005    99011    +6     
  Misses       7846     7846           
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8570/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8570/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.13% <44.44%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8570/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `92.63% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8570?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/ec2/responses/_base_response.py b/moto/ec2/responses/_base_response.py
index da11941fa..c4a45e508 100644
--- a/moto/ec2/responses/_base_response.py
+++ b/moto/ec2/responses/_base_response.py
@@ -2,7 +2,7 @@ from typing import Any, Dict, Optional
 
 from moto.core.responses import BaseResponse
 
-from ..exceptions import EmptyTagSpecError, InvalidParameter
+from ..exceptions import EC2ClientError, EmptyTagSpecError, InvalidParameter
 from ..utils import convert_tag_spec
 
 
@@ -15,9 +15,14 @@ class EC2BaseResponse(BaseResponse):
 
     def _filters_from_querystring(self) -> Dict[str, str]:
         # [{"Name": x1, "Value": y1}, ..]
-        _filters = self._get_multi_param("Filter.")
+        _filters = self._get_multi_param("Filter.", skip_result_conversion=True)
         # return {x1: y1, ...}
-        return {f["Name"]: f["Value"] for f in _filters}
+        try:
+            return {f["Name"]: f.get("Value", []) for f in _filters}
+        except KeyError:
+            raise EC2ClientError(
+                "InvalidParameterValue", "The filter 'null' is invalid."
+            )
 
     def _parse_tag_specification(
         self, expected_type: Optional[str] = None
diff --git a/moto/utilities/utils.py b/moto/utilities/utils.py
index 1b4ccb53f..44908f86e 100644
--- a/moto/utilities/utils.py
+++ b/moto/utilities/utils.py
@@ -98,6 +98,10 @@ def filter_resources(
                 ):
                     result.remove(resource)
                     break
+            elif attrs[0] in filters:
+                # In case the filter exists but the value of the filter is empty, the filter shouldn't match
+                result.remove(resource)
+                break
     return result
 
 


Test patch:
diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py
index 23a01c0de..b7d60b25d 100644
--- a/tests/test_ec2/test_amis.py
+++ b/tests/test_ec2/test_amis.py
@@ -1337,6 +1337,32 @@ def test_ami_filter_by_unknown_ownerid():
     assert len(images) == 0
 
 
+@mock_aws()
+def test_ami_filter_without_value():
+    client = boto3.client("ec2", region_name="us-east-1")
+    ec2_resource = boto3.resource("ec2", region_name="us-west-1")
+
+    image_name = str(uuid4())[0:12]
+
+    instance = ec2_resource.create_instances(
+        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1
+    )[0]
+    instance.create_image(Name=image_name)
+
+    images = client.describe_images(Filters=[{"Name": "owner-alias", "Values": []}])[
+        "Images"
+    ]
+    assert len(images) == 0
+
+    images = client.describe_images(Filters=[{"Name": "owner-alias"}])["Images"]
+    assert len(images) == 0
+
+    with pytest.raises(ClientError) as err:
+        client.describe_images(Filters=[{"Values": ["123"]}])
+
+    assert err.value.response["Error"]["Code"] == "InvalidParameterValue"
+
+
 @mock_aws
 def test_describe_images_dryrun():
     client = boto3.client("ec2", region_name="us-east-1")
diff --git a/tests/test_ec2/test_availability_zones_and_regions.py b/tests/test_ec2/test_availability_zones_and_regions.py
index d092bcde4..6724c4c06 100644
--- a/tests/test_ec2/test_availability_zones_and_regions.py
+++ b/tests/test_ec2/test_availability_zones_and_regions.py
@@ -77,6 +77,17 @@ def test_availability_zones__parameters():
     assert len(zones) == 1
     assert zones[0]["ZoneId"] == "use1-az1"
 
+    # This shouldn't raise an unhandled exception
+    zones = us_east.describe_availability_zones(Filters=[{"Name": "state"}])[
+        "AvailabilityZones"
+    ]
+    assert len(zones) == 0
+
+    zones = us_east.describe_availability_zones(
+        Filters=[{"Name": "state", "Values": []}]
+    )["AvailabilityZones"]
+    assert len(zones) == 0
+
 
 @mock_aws
 def test_describe_availability_zones_dryrun():
diff --git a/tests/test_ec2/test_subnets.py b/tests/test_ec2/test_subnets.py
index 850277ed3..e83c26668 100644
--- a/tests/test_ec2/test_subnets.py
+++ b/tests/test_ec2/test_subnets.py
@@ -330,6 +330,16 @@ def test_get_subnets_filtering():
         with pytest.raises(NotImplementedError):
             client.describe_subnets(Filters=filters)
 
+    # Filter without a Value.
+    subnets_with_invalid_filter = client.describe_subnets(Filters=[{"Name": "vpc-id"}])[
+        "Subnets"
+    ]
+    assert len(subnets_with_invalid_filter) == 0
+    subnets_with_invalid_filter = client.describe_subnets(
+        Filters=[{"Name": "vpc-id", "Values": []}]
+    )["Subnets"]
+    assert len(subnets_with_invalid_filter) == 0
+
 
 @mock_aws
 def test_create_subnet_response_fields():