From e83836b7191aa3f35dedea67cb2206476c8e0941 Mon Sep 17 00:00:00 2001 From: Nick Spain Date: Mon, 16 Sep 2024 09:03:17 +1000 Subject: [PATCH] Retry bad nonce errors after generating a new nonce Retry `urn:ietf:params:acme:error:badNonce` errors as they are defined as retryable[^1]. Requests failing with a bad nonce error will be retried three time retried three times before the failure is returned to the caller. This implenments the `BytesBody` trait for `bytes::Bytes` as we need to consume the response body to be able check if the failure was due to a bad nonce which required parsing the body. The body is only consumed if the response status is bad request, in all other cases the body is still lazily consumable. [^1]: https://datatracker.ietf.org/doc/html/rfc8555/#section-6.5 --- src/lib.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index f7fffee..ed63fdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -490,6 +490,43 @@ impl Client { } async fn post( + &self, + payload: Option<&impl Serialize>, + mut nonce: Option, + signer: &impl Signer, + url: &str, + ) -> Result { + let mut retries = 3; + loop { + let mut response = self + .post_attempt(payload, nonce.clone(), signer, url) + .await?; + if response.parts.status != StatusCode::BAD_REQUEST { + return Ok(response); + } + let body = response.body.into_bytes().await.map_err(Error::Other)?; + let problem = serde_json::from_slice::(&body)?; + if let Some("urn:ietf:params:acme:error:badNonce") = problem.r#type.as_deref() { + retries -= 1; + if retries != 0 { + // Retrieve the new nonce. If it isn't there (it + // should be, the spec requires it) then we will + // manually refresh a new one in `post_attempt` + // due to `nonce` being `None` but getting it from + // the response saves us making that request. + nonce = nonce_from_response(&response); + continue; + } + } + + return Ok(BytesResponse { + parts: response.parts, + body: Box::new(body), + }); + } + } + + async fn post_attempt( &self, payload: Option<&impl Serialize>, nonce: Option, @@ -789,6 +826,13 @@ where } } +#[async_trait] +impl BytesBody for Bytes { + async fn into_bytes(&mut self) -> Result> { + Ok(self.to_owned()) + } +} + /// Object safe body trait #[async_trait] pub trait BytesBody: Send {