Repository: getmoto/moto
Issue category: Bug Report

moto/core/responses.py `_get_multi_param` bug

> Guidance on **how to reproduce the issue**. Ideally, this should be a
     *small* code sample that can be run immediately by the maintainers.
     Failing that, let us know what you're doing, how often it happens, what
     environment you're using, etc. Be thorough: it prevents us needing to ask
     further questions.
     
- try to execute this code:
```python
fleet_request = {
        'LaunchTemplateConfigs': [
            {
                'LaunchTemplateSpecification': {
                    'LaunchTemplateName': 'test-template-name',
                    'Version': '$Default'
                },
                'Overrides': [ { "InstanceType": "t3.nano" } ]
            }
        ]
    }
ec2_client.create_fleet(**fleet_request)
```

>  Tell us **what you expected to happen**. When we run your example code,
     what are we expecting to happen? What does "success" look like for your
     code?


https://github.com/getmoto/moto/blob/1b91aee1de3d26902c5e79a3adefb8f391fb9876/moto/ec2/responses/fleets.py#L33
```python
# Expected:
(Pdb) print(launch_template_configs)
[{'LaunchTemplateSpecification': {'LaunchTemplateName': 'test-template-name', 'Version': '$Default'}, 'Overrides': [ {'InstanceType': 't3.nano'} ]}]
```

>   Tell us **what actually happens**. It's not helpful for you to say "it
     doesn't work" or "it fails". Tell us *how* it fails: do you get an
     exception? A hang? How was the actual result different from your expected
     result?

```python
# Actually happens, see Overrides section
(Pdb) print(launch_template_configs)
[{'LaunchTemplateSpecification': {'LaunchTemplateName': 'test-template-name', 'Version': '$Default'}, 'Overrides': [ 't3.nano' ]}]
```

https://github.com/getmoto/moto/blob/1b91aee1de3d26902c5e79a3adefb8f391fb9876/moto/core/responses.py#L701
```python
(Pdb) print(self.querystring)
OrderedDict({'Action': ['CreateFleet'],
 'Version': ['2016-11-15'],
 'Type': ['request'],
 'ValidUntil': ['2024-11-14T08:57:24.289912Z'],
 /* ... cropped ... */
 'LaunchTemplateConfigs.1.LaunchTemplateSpecification.LaunchTemplateName': ['test-template-name'],
 'LaunchTemplateConfigs.1.LaunchTemplateSpecification.Version': ['$Default'],
 'LaunchTemplateConfigs.1.Overrides.1.InstanceType': ['t3.nano']
 /* ... cropped ... */})
```

```python
# how it fails
            launch_template_data = launch_template.latest_version().data
            new_launch_template = launch_template_data.copy()
            if config.get("Overrides"):
                for override in config["Overrides"]:
>                   new_launch_template.update(override)
E                   ValueError: dictionary update sequence element #0 has length 1; 2 is required

.venv/lib/python3.13/site-packages/moto/ec2/models/fleets.py:73: ValueError
```

   

>  Tell us **what version of Moto you're using**, and
     **how you installed it**. Tell us whether you're using standalone server
     mode or the Python mocks. If you are using the Python mocks, include the
     version of boto/boto3/botocore.

- `5.0.20`



Functions to edit:
moto/ec2/responses/fleets.py:Fleets.create_fleet

Patch:
diff --git a/moto/ec2/responses/fleets.py b/moto/ec2/responses/fleets.py
index e63a74322812..ff44fa84b6ff 100644
--- a/moto/ec2/responses/fleets.py
+++ b/moto/ec2/responses/fleets.py
@@ -33,6 +33,22 @@ def create_fleet(self) -> str:
         launch_template_configs = self._get_multi_param(
             param_prefix="LaunchTemplateConfigs"
         )
+        # Our multi_param function doesn't understand when to return a list, a string, or a dictionary
+        # So we'll have to help it a little, by explicitly asking for a dict if we're getting anything else back
+        for idx, config in enumerate(launch_template_configs, start=1):
+            if (overrides := config.get("Overrides")) and not isinstance(
+                overrides, dict
+            ):
+                config["Overrides"] = self._get_multi_param(
+                    f"LaunchTemplateConfigs.{idx}.Overrides",
+                    skip_result_conversion=True,
+                )
+            if (spec := config.get("LaunchTemplateSpecification")) and not isinstance(
+                spec, dict
+            ):
+                config["LaunchTemplateSpecification"] = self._get_multi_param_dict(
+                    f"LaunchTemplateConfigs.{idx}.LaunchTemplateSpecification"
+                )
 
         excess_capacity_termination_policy = self._get_param(
             "ExcessCapacityTerminationPolicy"

Test patch: diff --git a/tests/test_ec2/test_fleets.py b/tests/test_ec2/test_fleets.py
index e2db4ceba2c0..a8080ce80eb3 100644
--- a/tests/test_ec2/test_fleets.py
+++ b/tests/test_ec2/test_fleets.py
@@ -328,6 +328,35 @@ def test_create_fleet_request_with_tags():
             assert tag in instance["Tags"]
 
 
+@mock_aws
+@pytest.mark.parametrize(
+    "overrides", [{"InstanceType": "t2.nano"}, {"AvailabilityZone": "us-west-1"}]
+)
+def test_create_fleet_using_launch_template_config__overrides_single(overrides):
+    conn = boto3.client("ec2", region_name="us-east-2")
+
+    template_id, _ = get_launch_template(conn, instance_type="t2.medium")
+
+    fleet_id = conn.create_fleet(
+        LaunchTemplateConfigs=[
+            {
+                "LaunchTemplateSpecification": {"LaunchTemplateId": template_id},
+                # Verify we parse the incoming parameters correctly,
+                # even when only supplying a dictionary with a single key
+                "Overrides": [overrides],
+            },
+        ],
+        TargetCapacitySpecification={
+            "DefaultTargetCapacityType": "spot",
+            "TotalTargetCapacity": 1,
+        },
+    )["FleetId"]
+
+    fleet = conn.describe_fleets(FleetIds=[fleet_id])["Fleets"][0]
+    lt_config = fleet["LaunchTemplateConfigs"][0]
+    assert lt_config["Overrides"] == [overrides]
+
+
 @mock_aws
 def test_create_fleet_using_launch_template_config__overrides():
     conn = boto3.client("ec2", region_name="us-east-2")