diff --git a/acme/acme/client.py b/acme/acme/client.py index b5db57235..cdfa43ee0 100644 --- a/acme/acme/client.py +++ b/acme/acme/client.py @@ -658,8 +658,25 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes self._add_nonce(self.head(url)) return self._nonces.pop() - def post(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs): - """POST object wrapped in `.JWS` and check response.""" + def post(self, *args, **kwargs): + """POST object wrapped in `.JWS` and check response. + + If the server responded with a badNonce error, the request will + be retried once. + + """ + should_retry = True + while True: + try: + return self._post_once(*args, **kwargs) + except messages.Error as error: + if should_retry and error.code == 'badNonce': + logger.debug('Retrying request after error:\n%s', error) + should_retry = False + else: + raise + + def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs): data = self._wrap_in_jws(obj, self._get_nonce(url)) kwargs.setdefault('headers', {'Content-Type': content_type}) response = self._send_request('POST', url, data=data, **kwargs) diff --git a/acme/acme/client_test.py b/acme/acme/client_test.py index e0403ef28..5672f4200 100644 --- a/acme/acme/client_test.py +++ b/acme/acme/client_test.py @@ -616,7 +616,9 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): self.wrapped_obj = mock.MagicMock() self.content_type = mock.sentinel.content_type - self.all_nonces = [jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2')] + self.all_nonces = [ + jose.b64encode(b'Nonce'), + jose.b64encode(b'Nonce2'), jose.b64encode(b'Nonce3')] self.available_nonces = self.all_nonces[:] def send_request(*args, **kwargs): @@ -664,7 +666,7 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): self.net._wrap_in_jws.assert_called_once_with( self.obj, jose.b64decode(self.all_nonces.pop())) - assert not self.available_nonces + self.available_nonces = [] self.assertRaises(errors.MissingNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) self.net._wrap_in_jws.assert_called_with( @@ -680,6 +682,25 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): self.assertRaises(errors.BadNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) + def test_post_failed_retry(self): + check_response = mock.MagicMock() + check_response.side_effect = messages.Error.with_code('badNonce') + + # pylint: disable=protected-access + self.net._check_response = check_response + self.assertRaises(messages.Error, self.net.post, 'uri', + self.obj, content_type=self.content_type) + + def test_post_successful_retry(self): + check_response = mock.MagicMock() + check_response.side_effect = [messages.Error.with_code('badNonce'), + self.checked_response] + + # pylint: disable=protected-access + self.net._check_response = check_response + self.assertEqual(self.checked_response, self.net.post( + 'uri', self.obj, content_type=self.content_type)) + def test_head_get_post_error_passthrough(self): self.send_request.side_effect = requests.exceptions.RequestException for method in self.net.head, self.net.get: