Skip to content

Commit 4e18116

Browse files
committed
Add support for Retry-After headers in poll()
1 parent db427bb commit 4e18116

File tree

3 files changed

+43
-4
lines changed

3 files changed

+43
-4
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ aws-lc-rs = { version = "1.8.0", optional = true }
2525
base64 = "0.22"
2626
bytes = "1"
2727
http = "1"
28+
httpdate = "1.0.3"
2829
http-body = "1"
2930
http-body-util = "0.1.2"
3031
hyper = { version = "1.3.1", features = ["client", "http1", "http2"], optional = true }

src/account.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ impl Account {
214214
Ok(Order {
215215
account: self.inner.clone(),
216216
nonce,
217+
retry_after: None,
217218
state,
218219
url: order_url.ok_or("no order URL found")?,
219220
})
@@ -229,6 +230,7 @@ impl Account {
229230
Ok(Order {
230231
account: self.inner.clone(),
231232
nonce: nonce_from_response(&rsp),
233+
retry_after: None,
232234
// Order of fields matters! We return errors from Problem::check
233235
// before emitting an error if there is no order url. Or the
234236
// simple no url error hides the causing error in `Problem::check`.

src/order.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use std::ops::{ControlFlow, Deref};
2+
use std::str::FromStr;
23
use std::sync::Arc;
3-
use std::time::Duration;
4+
use std::time::{Duration, SystemTime};
45
use std::{fmt, slice};
56

67
use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine};
8+
use http::header::RETRY_AFTER;
9+
use httpdate::HttpDate;
710
#[cfg(feature = "rcgen")]
811
use rcgen::{CertificateParams, DistinguishedName, KeyPair};
912
use serde::Serialize;
@@ -14,7 +17,7 @@ use crate::types::{
1417
Authorization, AuthorizationState, AuthorizationStatus, AuthorizedIdentifier, Challenge,
1518
ChallengeType, Empty, FinalizeRequest, OrderState, OrderStatus, Problem,
1619
};
17-
use crate::{Error, Key, crypto, nonce_from_response};
20+
use crate::{BytesResponse, Error, Key, crypto, nonce_from_response};
1821

1922
/// An ACME order as described in RFC 8555 (section 7.1.3)
2023
///
@@ -27,6 +30,7 @@ use crate::{Error, Key, crypto, nonce_from_response};
2730
pub struct Order {
2831
pub(crate) account: Arc<AccountInner>,
2932
pub(crate) nonce: Option<String>,
33+
pub(crate) retry_after: Option<SystemTime>,
3034
pub(crate) url: String,
3135
pub(crate) state: OrderState,
3236
}
@@ -163,8 +167,9 @@ impl Order {
163167
/// Yields the [`OrderStatus`] immediately if `Ready` or `Invalid`, or after `tries` attempts.
164168
pub async fn poll(&mut self, retries: &RetryPolicy) -> Result<OrderStatus, Error> {
165169
let mut retrying = retries.state();
170+
self.retry_after = None;
166171
loop {
167-
if let ControlFlow::Break(()) = retrying.wait().await {
172+
if let ControlFlow::Break(()) = retrying.wait(self.retry_after.take()).await {
168173
return Ok(self.state.status);
169174
}
170175

@@ -185,6 +190,7 @@ impl Order {
185190
.await?;
186191

187192
self.nonce = nonce_from_response(&rsp);
193+
self.retry_after = retry_after(&rsp);
188194
self.state = Problem::check::<OrderState>(rsp).await?;
189195
Ok(&self.state)
190196
}
@@ -544,14 +550,44 @@ struct RetryState {
544550
}
545551

546552
impl RetryState {
547-
async fn wait(&mut self) -> ControlFlow<(), ()> {
553+
async fn wait(&mut self, after: Option<SystemTime>) -> ControlFlow<(), ()> {
548554
if self.tries == 0 {
549555
return ControlFlow::Break(());
550556
}
551557

558+
if let Some(after) = after {
559+
if let Ok(delay) = after.duration_since(SystemTime::now()) {
560+
sleep(delay).await;
561+
self.tries -= 1;
562+
return ControlFlow::Continue(());
563+
}
564+
}
565+
552566
sleep(self.delay).await;
553567
self.delay *= 2;
554568
self.tries -= 1;
555569
ControlFlow::Continue(())
556570
}
557571
}
572+
573+
/// Parse the `Retry-After` header from the response
574+
///
575+
/// <https://httpwg.org/specs/rfc9110.html#field.retry-after>
576+
///
577+
/// # Syntax
578+
///
579+
/// Retry-After = HTTP-date / delay-seconds
580+
/// delay-seconds = 1*DIGIT
581+
fn retry_after(rsp: &BytesResponse) -> Option<SystemTime> {
582+
let value = rsp.parts.headers.get(RETRY_AFTER)?.to_str().ok()?.trim();
583+
if value.is_empty() {
584+
return None;
585+
}
586+
587+
Some(match u64::from_str(value) {
588+
// `delay-seconds` is a number of seconds to wait
589+
Ok(secs) => SystemTime::now() + Duration::from_secs(secs),
590+
// `HTTP-date` looks like `Fri, 31 Dec 1999 23:59:59 GMT`
591+
Err(_) => SystemTime::from(HttpDate::from_str(value).ok()?),
592+
})
593+
}

0 commit comments

Comments
 (0)