From 472ab10baeb9a096fbdeac052bc618f403f9d55c Mon Sep 17 00:00:00 2001 From: Andrew Miller Date: Wed, 31 Dec 2025 13:23:21 -0500 Subject: [PATCH] fix(namecheap): Fix XML namespace and DNSRecord type conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs that caused all existing DNS records to be deleted: 1. XML namespace mismatch: XPath queries used https:// but Namecheap API returns http:// namespace, causing get_dns_records() to always return 0 records. 2. DNSRecord to dict conversion: create_dns_record() and delete_dns_record() passed DNSRecord objects to _set_dns_records() which expected dicts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../scripts/dns_providers/namecheap.py | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/custom-domain/dstack-ingress/scripts/dns_providers/namecheap.py b/custom-domain/dstack-ingress/scripts/dns_providers/namecheap.py index cc5f216..4aca88c 100644 --- a/custom-domain/dstack-ingress/scripts/dns_providers/namecheap.py +++ b/custom-domain/dstack-ingress/scripts/dns_providers/namecheap.py @@ -91,7 +91,7 @@ def _make_request(self, command: str, **params) -> Dict: root = ET.fromstring(response.content) # Check for API errors - errors = root.find('.//{https://api.namecheap.com/xml.response}Errors') + errors = root.find('.//{http://api.namecheap.com/xml.response}Errors') if errors is not None and len(errors) > 0: error_messages = [] for error in errors: @@ -154,7 +154,7 @@ def get_dns_records( # Parse the host records from XML response records = [] - host_elements = result["result"].findall('.//{https://api.namecheap.com/xml.response}host') + host_elements = result["result"].findall('.//{http://api.namecheap.com/xml.response}host') for host in host_elements: record_name = host.get("Name") @@ -215,43 +215,46 @@ def create_dns_record(self, record: DNSRecord) -> bool: else: hostname = record.name.replace("." + sld + "." + tld, "") - # Remove existing records of the same type and name - filtered_records = [ - r for r in existing_records - if not (r.name == record.name and r.type == record.type) - ] - + # Remove existing records of the same type and name, convert to dicts + all_records = [] + for r in existing_records: + if r.name == record.name and r.type == record.type: + continue + r_hostname = "@" if r.name == sld + "." + tld else r.name.replace("." + sld + "." + tld, "") + d = {"HostName": r_hostname, "RecordType": r.type.value, "Address": r.content, "TTL": str(r.ttl)} + if r.type == RecordType.MX and r.priority: + d["MXPref"] = str(r.priority) + all_records.append(d) + # Add new record - new_record = { - "HostName": hostname, - "RecordType": record.type.value, - "Address": record.content, - "TTL": str(record.ttl) - } - + new_record = {"HostName": hostname, "RecordType": record.type.value, "Address": record.content, "TTL": str(record.ttl)} if record.type == RecordType.MX and record.priority: new_record["MXPref"] = str(record.priority) - - filtered_records.append(new_record) - - # Set all records - return self._set_dns_records(sld, tld, filtered_records) + all_records.append(new_record) + + return self._set_dns_records(sld, tld, all_records) def delete_dns_record(self, record_id: str, domain: str) -> bool: """Delete a DNS record.""" - # Namecheap doesn't support individual record deletion - # We need to get all records, remove the one with the matching ID, and set them all domain_info = self._get_domain_info(domain) if not domain_info: return False - + sld, tld = domain_info existing_records = self.get_dns_records(domain) - - # Remove the record with the matching ID - filtered_records = [r for r in existing_records if r.id != record_id] - - return self._set_dns_records(sld, tld, filtered_records) + + # Remove record with matching ID, convert rest to dicts + all_records = [] + for r in existing_records: + if r.id == record_id: + continue + r_hostname = "@" if r.name == sld + "." + tld else r.name.replace("." + sld + "." + tld, "") + d = {"HostName": r_hostname, "RecordType": r.type.value, "Address": r.content, "TTL": str(r.ttl)} + if r.type == RecordType.MX and r.priority: + d["MXPref"] = str(r.priority) + all_records.append(d) + + return self._set_dns_records(sld, tld, all_records) def create_caa_record(self, caa_record: CAARecord) -> bool: """Create a CAA record."""