This commit is contained in:
John Muirhead-Gould 2026-07-01 19:41:35 -04:00 committed by GitHub
commit 13bf141ea5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 45 additions and 0 deletions

View file

@ -111,6 +111,10 @@ class _CloudflareClient:
"""
Add a TXT record using the supplied information.
If a TXT record with the same name and content already exists (e.g. from a
previous failed attempt or a reused authorization), the existing record is
left in place and this method returns successfully.
:param str domain: The domain to use to look up the Cloudflare zone.
:param str record_name: The record name (typically beginning with '_acme-challenge.').
:param str record_content: The record content (typically the challenge validation).
@ -120,6 +124,14 @@ class _CloudflareClient:
zone_id = self._find_zone_id(domain)
# Check if the record already exists (e.g. from a prior unclean termination
# or the ACME server reusing a pending authorization).
existing_record_id = self._find_txt_record_id(zone_id, record_name, record_content)
if existing_record_id:
logger.debug('TXT record already exists with record_id: %s; no action needed.',
existing_record_id)
return
data = {'type': 'TXT',
'name': record_name,
'content': record_content,
@ -130,6 +142,15 @@ class _CloudflareClient:
self.cf.zones.dns_records.post(zone_id, data=data) # zones | pylint: disable=no-member
except CloudFlare.exceptions.CloudFlareAPIError as e:
code = int(e)
# Error 81057 means "Record already exists" — this can happen due to a
# race condition between the pre-check above and a concurrent create
# (e.g. parallel certbot invocations for the same domain).
if code == 81057:
logger.debug('TXT record was created concurrently (error 81057); '
'treating as success.')
return
hint = None
if code == 1009:

View file

@ -112,6 +112,7 @@ class CloudflareClientTest(unittest.TestCase):
def test_add_txt_record(self):
self.cf.zones.get.return_value = [{'id': self.zone_id}]
self.cf.zones.dns_records.get.return_value = []
self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content,
self.record_ttl)
@ -125,8 +126,31 @@ class CloudflareClientTest(unittest.TestCase):
assert self.record_content == post_data['content']
assert self.record_ttl == post_data['ttl']
def test_add_txt_record_already_exists(self):
self.cf.zones.get.return_value = [{'id': self.zone_id}]
self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content,
self.record_ttl)
# Should not attempt to create since it already exists
self.cf.zones.dns_records.post.assert_not_called()
def test_add_txt_record_duplicate_race_condition(self):
self.cf.zones.get.return_value = [{'id': self.zone_id}]
# Pre-check finds nothing (record doesn't exist yet)
self.cf.zones.dns_records.get.return_value = []
# But create fails because another process created it concurrently
self.cf.zones.dns_records.post.side_effect = \
CloudFlare.exceptions.CloudFlareAPIError(81057, '', '')
# Should not raise — treat duplicate as success
self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content,
self.record_ttl)
def test_add_txt_record_error(self):
self.cf.zones.get.return_value = [{'id': self.zone_id}]
self.cf.zones.dns_records.get.return_value = []
self.cf.zones.dns_records.post.side_effect = CloudFlare.exceptions.CloudFlareAPIError(1009, '', '')