ephemeris_release/adapters/
registry.rs1use std::error::Error as StdError;
4use std::time::Duration;
5
6use ephemeris_token::Token;
7use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderValue, InvalidHeaderValue};
8use reqwest::{Client, StatusCode, Url};
9use tokio::time;
10
11const TIMEOUT: Duration = Duration::from_secs(30);
12const ATTEMPTS: u32 = 4;
13const INITIAL_DELAY: Duration = Duration::from_millis(250);
14
15#[derive(Debug)]
21pub(crate) struct HttpRegistry {
22 client: Client,
23 authorization: Option<HeaderValue>,
24}
25
26#[derive(Debug, thiserror::Error)]
28pub enum RegistryError {
29 #[error("cannot build the registry HTTP client")]
31 Client(#[source] reqwest::Error),
32 #[error("the registry token cannot be sent as a header value")]
34 InvalidToken(#[source] InvalidHeaderValue),
35 #[error("registry URL cannot address {path:?}")]
37 InvalidPath {
38 path: String,
40 #[source]
42 source: url::ParseError,
43 },
44 #[error("registry request failed")]
46 Request(#[source] reqwest::Error),
47 #[error("registry answered with an unreadable body")]
49 Body(#[source] facet_json::DeserializeError),
50 #[error("registry answered {status}: {body}")]
52 Response {
53 status: u16,
55 body: String,
57 },
58}
59
60impl HttpRegistry {
61 pub(crate) fn new(scheme: &str, token: Option<&Token>) -> Result<Self, RegistryError> {
64 Self::build(token.map(|token| format!("{scheme} {}", token.expose().trim())))
65 }
66
67 pub(crate) fn bare(token: &Token) -> Result<Self, RegistryError> {
69 Self::build(Some(token.expose().trim().to_owned()))
70 }
71
72 fn build(authorization: Option<String>) -> Result<Self, RegistryError> {
73 let authorization = authorization
74 .map(|text| {
75 let mut value =
76 HeaderValue::from_str(&text).map_err(RegistryError::InvalidToken)?;
77 value.set_sensitive(true);
78 Ok(value)
79 })
80 .transpose()?;
81 let client = Client::builder()
82 .user_agent(concat!("ephemeris-release/", env!("CARGO_PKG_VERSION")))
83 .timeout(TIMEOUT)
84 .build()
85 .map_err(RegistryError::Client)?;
86 Ok(Self {
87 client,
88 authorization,
89 })
90 }
91
92 pub(crate) async fn get(&self, url: &Url) -> Result<Option<String>, RegistryError> {
94 self.lookup(url).await
95 }
96
97 pub(crate) async fn put(&self, url: &Url, body: Vec<u8>) -> Result<String, RegistryError> {
102 let mut request = self
103 .client
104 .put(url.clone())
105 .header(CONTENT_TYPE, "application/octet-stream")
106 .body(body);
107 if let Some(authorization) = &self.authorization {
108 request = request.header(AUTHORIZATION, authorization.clone());
109 }
110 let response = request.send().await.map_err(RegistryError::Request)?;
111 let status = response.status();
112 let answer = response.text().await.map_err(RegistryError::Request)?;
113 if status.is_success() {
114 Ok(answer)
115 } else {
116 Err(RegistryError::Response {
117 status: status.as_u16(),
118 body: answer,
119 })
120 }
121 }
122
123 async fn lookup(&self, url: &Url) -> Result<Option<String>, RegistryError> {
124 let mut attempt = 1;
125 let mut delay = INITIAL_DELAY;
126 loop {
127 match self.attempt(url).await {
128 Err(error) if error.retryable() && attempt < ATTEMPTS => {
129 error.log_retry(attempt);
130 time::sleep(delay).await;
131 delay = delay.saturating_mul(2);
132 attempt = attempt.saturating_add(1);
133 }
134 other => return other,
135 }
136 }
137 }
138
139 async fn attempt(&self, url: &Url) -> Result<Option<String>, RegistryError> {
140 let mut request = self.client.get(url.clone());
141 if let Some(authorization) = &self.authorization {
142 request = request.header(AUTHORIZATION, authorization.clone());
143 }
144 let response = request.send().await.map_err(RegistryError::Request)?;
145 let status = response.status();
146 if status == StatusCode::NOT_FOUND {
147 return Ok(None);
148 }
149 let body = response.text().await.map_err(RegistryError::Request)?;
150 if status.is_success() {
151 Ok(Some(body))
152 } else {
153 Err(RegistryError::Response {
154 status: status.as_u16(),
155 body,
156 })
157 }
158 }
159}
160
161impl RegistryError {
162 fn log_retry(&self, attempt: u32) {
163 let cause: &dyn StdError = self;
164 tracing::warn!(error = cause, attempt, "registry lookup failed, retrying");
165 }
166
167 const fn retryable(&self) -> bool {
168 match self {
169 Self::Request(_) => true,
170 Self::Response { status, body: _ } => *status == 429 || *status >= 500,
171 Self::Client(_)
172 | Self::InvalidToken(_)
173 | Self::Body(_)
174 | Self::InvalidPath { path: _, source: _ } => false,
175 }
176 }
177}