Repository: getmoto/moto

Issue:
Route53: exception when trying to delete non-existent tag
Hi,

I'm using aws-sdk V3.  
When trying to delete a non existing tag from hosted zone Route32 I get this error in the console:
```... char 'd' is not expected ..```

and this error in moto:
```
    response = self.full_dispatch_request()

  File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 919, in full_dispatch_request

    rv = self.handle_user_exception(e)

  File "/usr/local/lib/python3.13/site-packages/flask_cors/extension.py", line 176, in wrapped_function

    return cors_after_request(app.make_response(f(*args, **kwargs)))

                                                ~^^^^^^^^^^^^^^^^^

  File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 917, in full_dispatch_request

    rv = self.dispatch_request()

  File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 902, in dispatch_request

    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]

           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^

  File "/moto/moto/core/utils.py", line 119, in __call__

    result = self.callback(request, request.url, dict(request.headers))

  File "/moto/moto/route53/urls.py", line 13, in tag_response

    return Route53().list_or_change_tags_for_resource_request(

    

  File "/moto/moto/route53/responses.py", line 379, in list_or_change_tags_for_resource_request

    self.backend.change_tags_for_resource(type_, id_, tags)

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^

  File "/moto/moto/route53/models.py", line 678, in change_tags_for_resource

    del self.resource_tags[resource_id][tags["Key"]]

        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^

KeyError: 'initial-tag-key'

172.18.0.1 - - [04/Dec/2025 16:56:49] "POST /2013-04-01/tags/hostedzone/4Q2YKW7QKK518MMA0HQY47 HTTP/1.1" 500 -

Error on request:

Traceback (most recent call last):

  File "/usr/local/lib/python3.13/site-packages/werkzeug/serving.py", line 370, in run_wsgi

    execute(self.server.app)

    ~~~~~~~^^^^^^^^^^^^^^^^^

  File "/usr/local/lib/python3.13/site-packages/werkzeug/serving.py", line 331, in execute

    application_iter = app(environ, start_response)

  File "/moto/moto/moto_server/werkzeug_app.py", line 267, in __call__

    return backend_app(environ, start_response)

  File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 1536, in __call__

    return self.wsgi_app(environ, start_response)

           ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^

  File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 1514, in wsgi_app

    response = self.handle_exception(e)

  File "/usr/local/lib/python3.13/site-packages/flask_cors/extension.py", line 176, in wrapped_function

    return cors_after_request(app.make_response(f(*args, **kwargs)))

                                                ~^^^^^^^^^^^^^^^^^

  File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 1511, in wsgi_app

    response = self.full_dispatch_request()

  File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 919, in full_dispatch_request

    rv = self.handle_user_exception(e)

  File "/usr/local/lib/python3.13/site-packages/flask_cors/extension.py", line 176, in wrapped_function

    return cors_after_request(app.make_response(f(*args, **kwargs)))

                                                ~^^^^^^^^^^^^^^^^^

  File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 917, in full_dispatch_request

    rv = self.dispatch_request()

  File "/usr/local/lib/python3.13/site-packages/flask/app.py", line 902, in dispatch_request

    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]

           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^

  File "/moto/moto/core/utils.py", line 119, in __call__

    result = self.callback(request, request.url, dict(request.headers))

  File "/moto/moto/route53/urls.py", line 13, in tag_response

    return Route53().list_or_change_tags_for_resource_request(

    

  File "/moto/moto/route53/responses.py", line 379, in list_or_change_tags_for_resource_request

    self.backend.change_tags_for_resource(type_, id_, tags)

    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^

  File "/moto/moto/route53/models.py", line 678, in change_tags_for_resource

    del self.resource_tags[resource_id][tags["Key"]]

        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^

KeyError: 'initial-tag-key'
```

When trying the same operation from aws cli:
```$ aws route53 change-tags-for-resource --resource-type hostedzone --resource-id 4Q2YKW7QKK518MMA0HQY47 --remove-tag-keys owner```

I get this error:
```An error occurred (500) when calling the ChangeTagsForResource operation (reached max retries: 2): Internal Server Error```

Discussion hints:

Solution patch:
diff --git a/moto/route53/models.py b/moto/route53/models.py
index 9ab37c2f325d..6321e47e9b37 100644
--- a/moto/route53/models.py
+++ b/moto/route53/models.py
@@ -671,11 +671,12 @@ def change_tags_for_resource(
                 self.resource_tags[resource_id][key] = value
         else:
             if "Key" in tags:
-                if isinstance(tags["Key"], list):
-                    for key in tags["Key"]:
-                        del self.resource_tags[resource_id][key]
-                else:
-                    del self.resource_tags[resource_id][tags["Key"]]
+                if resource_id in self.resource_tags:
+                    if isinstance(tags["Key"], list):
+                        for key in tags["Key"]:
+                            self.resource_tags[resource_id].pop(key, None)
+                    else:
+                        self.resource_tags[resource_id].pop(tags["Key"], None)
 
     def list_tags_for_resource(self, resource_id: str) -> dict[str, str]:
         if resource_id in self.resource_tags:


Test patch:
diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py
index 96f6ec43fcf1..5228f786a07e 100644
--- a/tests/test_route53/test_route53.py
+++ b/tests/test_route53/test_route53.py
@@ -1882,3 +1882,30 @@ def test_create_hosted_zone_with_custom_id(
     hosted_zone = conn.create_hosted_zone(**payload)
 
     assert hosted_zone["HostedZone"]["Id"] == f"/hostedzone/{static_hosted_zone_id}"
+
+
+@mock_aws
+def test_delete_non_existent_tag_no_crash():
+    client = boto3.client("route53", region_name="us-east-1")
+
+    zone_name = "example.com."
+    zone = client.create_hosted_zone(
+        Name=zone_name, CallerReference="test-delete-tag-crash-repro"
+    )
+    zone_id = zone["HostedZone"]["Id"]
+
+    resource_id = zone_id.split("/")[-1]
+
+    try:
+        client.change_tags_for_resource(
+            ResourceType="hostedzone",
+            ResourceId=resource_id,
+            RemoveTagKeys=["initial-tag-key"],
+        )
+    except ClientError as e:
+        pytest.fail(f"Server crashed with error: {e}")
+
+    tags = client.list_tags_for_resource(
+        ResourceType="hostedzone", ResourceId=resource_id
+    )
+    assert len(tags["ResourceTagSet"]["Tags"]) == 0