diff --git a/moto/core/exceptions.py b/moto/core/exceptions.py
index 49d573f19..52c7fc01f 100644
--- a/moto/core/exceptions.py
+++ b/moto/core/exceptions.py
@@ -65,10 +65,10 @@ class RESTError(HTTPException):
self.content_type = "application/xml"
def get_headers(self, *args, **kwargs):
- return [
- ("X-Amzn-ErrorType", self.error_type or "UnknownError"),
- ("Content-Type", self.content_type),
- ]
+ return {
+ "X-Amzn-ErrorType": self.error_type or "UnknownError",
+ "Content-Type": self.content_type,
+ }
def get_body(self, *args, **kwargs):
return self.description
diff --git a/moto/route53/exceptions.py b/moto/route53/exceptions.py
index 94981dad6..575f96aae 100644
--- a/moto/route53/exceptions.py
+++ b/moto/route53/exceptions.py
@@ -51,6 +51,7 @@ class NoSuchHostedZone(Route53ClientError):
def __init__(self, host_zone_id):
message = f"No hosted zone found with ID: {host_zone_id}"
super().__init__("NoSuchHostedZone", message)
+ self.content_type = "text/xml"
class NoSuchQueryLoggingConfig(Route53ClientError):
diff --git a/moto/route53/models.py b/moto/route53/models.py
index 0e694b74e..cfa4d3273 100644
--- a/moto/route53/models.py
+++ b/moto/route53/models.py
@@ -441,7 +441,12 @@ class Route53Backend(BaseBackend):
return self.resource_tags[resource_id]
return {}
- def change_resource_record_sets(self, the_zone, change_list):
+ def list_resource_record_sets(self, zone_id, start_type, start_name):
+ the_zone = self.get_hosted_zone(zone_id)
+ return the_zone.get_record_sets(start_type, start_name)
+
+ def change_resource_record_sets(self, zoneid, change_list):
+ the_zone = self.get_hosted_zone(zoneid)
for value in change_list:
action = value["Action"]
record_set = value["ResourceRecordSet"]
@@ -504,7 +509,10 @@ class Route53Backend(BaseBackend):
return dnsname, zones
def get_hosted_zone(self, id_):
- return self.zones.get(id_.replace("/hostedzone/", ""))
+ the_zone = self.zones.get(id_.replace("/hostedzone/", ""))
+ if not the_zone:
+ raise NoSuchHostedZone(id_)
+ return the_zone
def get_hosted_zone_by_name(self, name):
for zone in self.list_hosted_zones():
@@ -513,6 +521,8 @@ class Route53Backend(BaseBackend):
return None
def delete_hosted_zone(self, id_):
+ # Verify it exists
+ self.get_hosted_zone(id_)
return self.zones.pop(id_.replace("/hostedzone/", ""), None)
def create_health_check(self, caller_reference, health_check_args):
diff --git a/moto/route53/responses.py b/moto/route53/responses.py
index d61d729d5..4b5f4bea7 100644
--- a/moto/route53/responses.py
+++ b/moto/route53/responses.py
@@ -1,20 +1,32 @@
"""Handles Route53 API requests, invokes method and returns response."""
+from functools import wraps
from urllib.parse import parse_qs, urlparse
from jinja2 import Template
import xmltodict
from moto.core.responses import BaseResponse
-from moto.core.exceptions import InvalidToken
-from moto.route53.exceptions import Route53ClientError, InvalidPaginationToken
+from moto.route53.exceptions import Route53ClientError
from moto.route53.models import route53_backend
XMLNS = "https://route53.amazonaws.com/doc/2013-04-01/"
+def error_handler(f):
+ @wraps(f)
+ def _wrapper(*args, **kwargs):
+ try:
+ return f(*args, **kwargs)
+ except Route53ClientError as e:
+ return e.code, e.get_headers(), e.get_body()
+
+ return _wrapper
+
+
class Route53(BaseResponse):
"""Handler for Route53 requests and responses."""
+ @error_handler
def list_or_create_hostzone_response(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
@@ -64,22 +76,21 @@ class Route53(BaseResponse):
template = Template(LIST_HOSTED_ZONES_BY_NAME_RESPONSE)
return 200, headers, template.render(zones=zones, dnsname=dnsname, xmlns=XMLNS)
+ @error_handler
def get_or_delete_hostzone_response(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
parsed_url = urlparse(full_url)
zoneid = parsed_url.path.rstrip("/").rsplit("/", 1)[1]
- the_zone = route53_backend.get_hosted_zone(zoneid)
- if not the_zone:
- return no_such_hosted_zone_error(zoneid, headers)
if request.method == "GET":
+ the_zone = route53_backend.get_hosted_zone(zoneid)
template = Template(GET_HOSTED_ZONE_RESPONSE)
-
return 200, headers, template.render(zone=the_zone)
elif request.method == "DELETE":
route53_backend.delete_hosted_zone(zoneid)
return 200, headers, DELETE_HOSTED_ZONE_RESPONSE
+ @error_handler
def rrset_response(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
@@ -87,9 +98,6 @@ class Route53(BaseResponse):
method = request.method
zoneid = parsed_url.path.rstrip("/").rsplit("/", 2)[1]
- the_zone = route53_backend.get_hosted_zone(zoneid)
- if not the_zone:
- return no_such_hosted_zone_error(zoneid, headers)
if method == "POST":
elements = xmltodict.parse(self.body)
@@ -104,9 +112,7 @@ class Route53(BaseResponse):
]["Change"]
]
- error_msg = route53_backend.change_resource_record_sets(
- the_zone, change_list
- )
+ error_msg = route53_backend.change_resource_record_sets(zoneid, change_list)
if error_msg:
return 400, headers, error_msg
@@ -121,7 +127,9 @@ class Route53(BaseResponse):
if start_type and not start_name:
return 400, headers, "The input is not valid"
- record_sets = the_zone.get_record_sets(start_type, start_name)
+ record_sets = route53_backend.list_resource_record_sets(
+ zoneid, start_type=start_type, start_name=start_name
+ )
return 200, headers, template.render(record_sets=record_sets)
def health_check_response(self, request, full_url, headers):
@@ -218,6 +226,7 @@ class Route53(BaseResponse):
template = Template(GET_CHANGE_RESPONSE)
return 200, headers, template.render(change_id=change_id, xmlns=XMLNS)
+ @error_handler
def list_or_create_query_logging_config_response(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
@@ -225,12 +234,10 @@ class Route53(BaseResponse):
json_body = xmltodict.parse(self.body)["CreateQueryLoggingConfigRequest"]
hosted_zone_id = json_body["HostedZoneId"]
log_group_arn = json_body["CloudWatchLogsLogGroupArn"]
- try:
- query_logging_config = route53_backend.create_query_logging_config(
- self.region, hosted_zone_id, log_group_arn
- )
- except Route53ClientError as r53error:
- return r53error.code, {}, r53error.description
+
+ query_logging_config = route53_backend.create_query_logging_config(
+ self.region, hosted_zone_id, log_group_arn
+ )
template = Template(CREATE_QUERY_LOGGING_CONFIG_RESPONSE)
headers["Location"] = query_logging_config.location
@@ -244,22 +251,14 @@ class Route53(BaseResponse):
hosted_zone_id = self._get_param("hostedzoneid")
next_token = self._get_param("nexttoken")
max_results = self._get_int_param("maxresults")
- try:
- try:
- # The paginator picks up named arguments, returns tuple.
- # pylint: disable=unbalanced-tuple-unpacking
- (
- all_configs,
- next_token,
- ) = route53_backend.list_query_logging_configs(
- hosted_zone_id=hosted_zone_id,
- next_token=next_token,
- max_results=max_results,
- )
- except InvalidToken as exc:
- raise InvalidPaginationToken() from exc
- except Route53ClientError as r53error:
- return r53error.code, {}, r53error.description
+
+ # The paginator picks up named arguments, returns tuple.
+ # pylint: disable=unbalanced-tuple-unpacking
+ (all_configs, next_token,) = route53_backend.list_query_logging_configs(
+ hosted_zone_id=hosted_zone_id,
+ next_token=next_token,
+ max_results=max_results,
+ )
template = Template(LIST_QUERY_LOGGING_CONFIGS_RESPONSE)
return (
@@ -272,18 +271,16 @@ class Route53(BaseResponse):
),
)
+ @error_handler
def get_or_delete_query_logging_config_response(self, request, full_url, headers):
self.setup_class(request, full_url, headers)
parsed_url = urlparse(full_url)
query_logging_config_id = parsed_url.path.rstrip("/").rsplit("/", 1)[1]
if request.method == "GET":
- try:
- query_logging_config = route53_backend.get_query_logging_config(
- query_logging_config_id
- )
- except Route53ClientError as r53error:
- return r53error.code, {}, r53error.description
+ query_logging_config = route53_backend.get_query_logging_config(
+ query_logging_config_id
+ )
template = Template(GET_QUERY_LOGGING_CONFIG_RESPONSE)
return (
200,
@@ -292,27 +289,10 @@ class Route53(BaseResponse):
)
elif request.method == "DELETE":
- try:
- route53_backend.delete_query_logging_config(query_logging_config_id)
- except Route53ClientError as r53error:
- return r53error.code, {}, r53error.description
+ route53_backend.delete_query_logging_config(query_logging_config_id)
return 200, headers, ""
-def no_such_hosted_zone_error(zoneid, headers=None):
- if not headers:
- headers = {}
- headers["X-Amzn-ErrorType"] = "NoSuchHostedZone"
- headers["Content-Type"] = "text/xml"
- error_response = f"""
-
- NoSuchHostedZone
- Zone {zoneid} Not Found
-
- """
- return 404, headers, error_response
-
-
LIST_TAGS_FOR_RESOURCE_RESPONSE = """
diff --git a/moto/route53/utils.py b/moto/route53/utils.py
index 8c0f38d27..2e7ae4387 100644
--- a/moto/route53/utils.py
+++ b/moto/route53/utils.py
@@ -1,4 +1,5 @@
"""Pagination control model for Route53."""
+from .exceptions import InvalidPaginationToken
PAGINATION_MODEL = {
"list_query_logging_configs": {
@@ -6,5 +7,6 @@ PAGINATION_MODEL = {
"limit_key": "max_results",
"limit_default": 100,
"unique_attribute": "hosted_zone_id",
+ "fail_on_invalid_token": InvalidPaginationToken,
},
}
diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py
index 4c3a2bcd9..ce8e9bde1 100644
--- a/tests/test_route53/test_route53.py
+++ b/tests/test_route53/test_route53.py
@@ -98,7 +98,7 @@ def test_get_unknown_hosted_zone():
err = ex.value.response["Error"]
err["Code"].should.equal("NoSuchHostedZone")
- err["Message"].should.equal("Zone unknown Not Found")
+ err["Message"].should.equal("No hosted zone found with ID: unknown")
# Has boto3 equivalent
@@ -224,7 +224,7 @@ def test_list_resource_record_set_unknown_zone():
err = ex.value.response["Error"]
err["Code"].should.equal("NoSuchHostedZone")
- err["Message"].should.equal("Zone abcd Not Found")
+ err["Message"].should.equal("No hosted zone found with ID: abcd")
@mock_route53