Respect Retry-After header when polling for order finalization (#10288)

Fixes #10273.

---------

Co-authored-by: Brad Warren <bmw@users.noreply.github.com>
This commit is contained in:
ohemorange 2025-05-15 09:24:52 -07:00 committed by GitHub
parent 5d03191493
commit 7a27a67cdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 26 additions and 5 deletions

View file

@ -281,7 +281,9 @@ class ClientV2Test(unittest.TestCase):
with pytest.raises(errors.Error, match="The certificate order failed"):
self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9))
def test_finalize_order_orderNotReady(self):
@mock.patch('acme.client.time.sleep')
@mock.patch('acme.client.datetime')
def test_finalize_order_orderNotReady(self, dt_mock, mock_sleep):
# https://github.com/certbot/certbot/issues/9766
updated_order_processing = self.order.update(status=messages.STATUS_PROCESSING)
updated_order_ready = self.order.update(status=messages.STATUS_READY)
@ -297,11 +299,18 @@ class ClientV2Test(unittest.TestCase):
updated_order_ready.to_json(),
updated_order_valid.to_json()]
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
dt_mock.timedelta = datetime.timedelta
self.response.headers['Retry-After'] = '50'
post = mock.MagicMock()
post.side_effect = [messages.Error.with_code('orderNotReady'), # first begin_finalization
self.response, # first poll_finalization poll --> still returns pending
# sleep 1
self.response, # first poll_finalization poll --> returns processing
# retry-after sleep here
self.response, # second poll_finalization poll --> returns ready
mock.MagicMock(), # second begin_finalization
# sleep 1
self.response, # third poll_finalization poll --> returns valid
self.response # fetch cert
]
@ -309,6 +318,7 @@ class ClientV2Test(unittest.TestCase):
self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9))
assert self.net.post.call_count == 6
assert mock_sleep.call_args_list == [((1,),), ((50,),), ((1,),)]
def test_finalize_order_otherErrorCode(self):
post = mock.MagicMock()

View file

@ -253,9 +253,10 @@ class ClientV2:
:returns: finalized order (with certificate)
:rtype: messages.OrderResource
"""
sleep_seconds: float = 1
while datetime.datetime.now() < deadline:
time.sleep(1)
if sleep_seconds > 0:
time.sleep(sleep_seconds)
response = self._post_as_get(orderr.uri)
body = messages.Order.from_json(response.json())
if body.status == messages.STATUS_INVALID:
@ -271,6 +272,7 @@ class ClientV2:
# fulfilled, and is awaiting finalization. Submit a finalization
# request.
self.begin_finalization(orderr)
sleep_seconds = 1
elif body.status == messages.STATUS_VALID and body.certificate is not None:
# "valid": The server has issued the certificate and provisioned its
# URL to the "certificate" field of the order. Download the
@ -282,6 +284,14 @@ class ClientV2:
alt_chains = [self._post_as_get(url).text for url in alt_chains_urls]
orderr = orderr.update(alternative_fullchains_pem=alt_chains)
return orderr
elif body.status == messages.STATUS_PROCESSING:
# "processing": The certificate is being issued. Send a POST-as-GET request after
# the time given in the Retry-After header field of the response, if any.
retry_after = self.retry_after(response, 1)
# Whatever Retry-After the ACME server requests, the polling must not take
# longer than the overall deadline
retry_after = min(retry_after, deadline)
sleep_seconds = (retry_after - datetime.datetime.now()).total_seconds()
raise errors.TimeoutError()
def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.datetime,

View file

@ -28,7 +28,8 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
`ready`, and resubmits finalization request before polling for `valid` to download
certificate. This conforms to RFC 8555 more accurately and avoids race conditions where
all authorizations are fulfilled but order has not yet transitioned to ready state on
the server when the finalization request is sent.
the server when the finalization request is sent. It also respects retry-after when
polling for finalization readiness.
* The --preferred-profile and --required-profile flags now have their values stored in
the renewal configuration so the same setting will be used on renewal.