Skip to content

Commit b4562d9

Browse files
committed
init retry-after
1 parent f18821e commit b4562d9

File tree

2 files changed

+128
-0
lines changed

2 files changed

+128
-0
lines changed

src/other/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
mod date;
44
mod expect;
55
mod referer;
6+
mod retry_after;
67
mod source_map;
78

89
pub use date::Date;
910
pub use expect::Expect;
1011
pub use referer::Referer;
12+
pub use retry_after::RetryAfter;
1113
pub use source_map::SourceMap;

src/other/retry_after.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use std::time::Duration;
2+
use std::{convert::TryInto, str::FromStr};
3+
4+
use crate::headers::{HeaderName, HeaderValue, Headers, RETRY_AFTER};
5+
6+
/// Indicate an alternate location for the returned data
7+
///
8+
/// [MDN Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
9+
///
10+
/// # Specifications
11+
///
12+
/// - [RFC 7231, section 3.1.4.2: Retry-After](https://tools.ietf.org/html/rfc7231#section-3.1.4.2)
13+
///
14+
/// # Examples
15+
///
16+
/// ```
17+
/// # fn main() -> http_types::Result<()> {
18+
/// #
19+
/// use http_types::other::RetryAfter;
20+
/// use http_types::{Response, Duration};
21+
///
22+
/// let loc = RetryAfter::new(Duration::parse("https://example.com/foo/bar")?);
23+
///
24+
/// let mut res = Response::new(200);
25+
/// loc.apply(&mut res);
26+
///
27+
/// let base_url = Duration::parse("https://example.com")?;
28+
/// let loc = RetryAfter::from_headers(base_url, res)?.unwrap();
29+
/// assert_eq!(
30+
/// loc.value(),
31+
/// Duration::parse("https://example.com/foo/bar")?.as_str()
32+
/// );
33+
/// #
34+
/// # Ok(()) }
35+
/// ```
36+
#[derive(Debug)]
37+
pub struct RetryAfter {
38+
dur: Duration,
39+
}
40+
41+
#[allow(clippy::len_without_is_empty)]
42+
impl RetryAfter {
43+
/// Create a new instance.
44+
pub fn new(dur: Duration) -> Self {
45+
Self {
46+
dur: location
47+
.try_into()
48+
.expect("could not convert into a valid URL"),
49+
}
50+
}
51+
52+
/// Create a new instance from headers.
53+
///
54+
/// `Retry-After` headers can provide both full and partial URLs. In
55+
/// order to always return fully qualified URLs, a base URL must be passed to
56+
/// reference the current environment. In HTTP/1.1 and above this value can
57+
/// always be determined from the request.
58+
pub fn from_headers<U>(base_url: U, headers: impl AsRef<Headers>) -> crate::Result<Option<Self>>
59+
where
60+
U: TryInto<Duration>,
61+
U::Error: std::fmt::Debug,
62+
{
63+
let headers = match headers.as_ref().get(RETRY_AFTER) {
64+
Some(headers) => headers,
65+
None => return Ok(None),
66+
};
67+
68+
// If we successfully parsed the header then there's always at least one
69+
// entry. We want the last entry.
70+
let location = headers.iter().last().unwrap();
71+
72+
let location = match Duration::from_str(location.as_str()) {
73+
Ok(url) => url,
74+
Err(_) => {
75+
let base_url = base_url
76+
.try_into()
77+
.expect("Could not convert base_url into a valid URL");
78+
let url = base_url.join(location.as_str())?;
79+
url
80+
}
81+
};
82+
Ok(Some(Self { dur: location }))
83+
}
84+
85+
/// Sets the header.
86+
pub fn apply(&self, mut headers: impl AsMut<Headers>) {
87+
headers.as_mut().insert(self.name(), self.value());
88+
}
89+
90+
/// Get the `HeaderName`.
91+
pub fn name(&self) -> HeaderName {
92+
RETRY_AFTER
93+
}
94+
95+
/// Get the `HeaderValue`.
96+
pub fn value(&self) -> HeaderValue {
97+
let output = format!("{}", self.dur);
98+
// SAFETY: the internal string is validated to be ASCII.
99+
unsafe { HeaderValue::from_bytes_unchecked(output.into()) }
100+
}
101+
}
102+
103+
#[cfg(test)]
104+
mod test {
105+
use super::*;
106+
use crate::headers::Headers;
107+
108+
// NOTE(yosh): I couldn't get a 400 test in because I couldn't generate any
109+
// invalid URLs. By default they get escaped, so ehhh -- I think it's fine.
110+
111+
#[test]
112+
fn smoke() -> crate::Result<()> {
113+
let loc = RetryAfter::new(Duration::parse("https://example.com/foo/bar")?);
114+
115+
let mut headers = Headers::new();
116+
loc.apply(&mut headers);
117+
118+
let base_url = Duration::parse("https://example.com")?;
119+
let loc = RetryAfter::from_headers(base_url, headers)?.unwrap();
120+
assert_eq!(
121+
loc.value(),
122+
Duration::parse("https://example.com/foo/bar")?.as_str()
123+
);
124+
Ok(())
125+
}
126+
}

0 commit comments

Comments
 (0)