1use std::time::Duration;
2
3use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, utf8_percent_encode};
4use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderValue, USER_AGENT};
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7use serde_json::Value;
8
9use crate::error::ApiError;
10use crate::{Error, Service};
11
12pub const CONSISTENCY_TOKEN_HEADER: &str = "Origin-Consistency-Token";
15
16const MAX_RESPONSE_BYTES: usize = 32 << 20;
18
19const FIRST_RETRY_DELAY: Duration = Duration::from_millis(200);
20const MAX_RETRY_DELAY: Duration = Duration::from_secs(10);
21
22pub use reqwest::Method;
24
25pub type Query = Vec<(String, String)>;
27
28#[derive(Clone, Debug)]
31pub struct Request {
32 method: Method,
33 path: String,
34 query: Query,
35 body: Option<Vec<u8>>,
36 idempotent: bool,
37 pub(crate) timeout_answers: bool,
40}
41
42impl Request {
43 pub fn new(method: Method, path: impl Into<String>) -> Self {
45 Self {
46 method,
47 path: path.into(),
48 query: Query::new(),
49 body: None,
50 idempotent: false,
51 timeout_answers: false,
52 }
53 }
54
55 pub fn with_query(mut self, query: Query) -> Self {
57 self.query = query;
58 self
59 }
60
61 pub fn with_body(mut self, body: Vec<u8>) -> Self {
63 self.body = Some(body);
64 self
65 }
66
67 pub fn idempotent(mut self) -> Self {
71 self.idempotent = true;
72 self
73 }
74}
75
76impl Service {
77 pub async fn call<T: DeserializeOwned>(&self, request: Request) -> Result<T, Error> {
80 let mut attempt = 1;
81 loop {
82 let err = match self.send(&request).await {
83 Ok(answer) => {
84 return serde_json::from_slice(&answer).map_err(|e| {
85 Error::Decode(format!(
86 "reading the response to {} {}: {e}",
87 request.method, request.path
88 ))
89 });
90 }
91 Err(err) => err,
92 };
93 if request.timeout_answers && is_timeout(&err) {
94 return Err(err);
95 }
96 if !request.idempotent || attempt >= self.client.inner.max_attempts {
97 return Err(err);
98 }
99 let Some(delay) = retry_delay(&err, attempt) else {
100 return Err(err);
101 };
102 tokio::time::sleep(delay).await;
103 attempt += 1;
104 }
105 }
106
107 async fn send(&self, request: &Request) -> Result<Vec<u8>, Error> {
109 let base = self.base.as_ref().map_err(|e| Error::Config(e.clone()))?;
110 let mut url = base.clone();
111 url.set_path(&format!(
112 "{}{}",
113 base.path().trim_end_matches('/'),
114 request.path
115 ));
116 if request.query.is_empty() {
117 url.set_query(None);
118 } else {
119 url.query_pairs_mut().clear().extend_pairs(&request.query);
120 }
121 let inner = &self.client.inner;
122 let token = inner.credentials.token().await?;
123 let mut builder = inner
124 .http
125 .request(request.method.clone(), url)
126 .header(AUTHORIZATION, format!("Bearer {token}"))
127 .header(ACCEPT, "application/json")
128 .header(USER_AGENT, &inner.user_agent);
129 if let Some(timeout) = inner.timeout {
130 builder = builder.timeout(timeout);
131 }
132 if let Some(body) = &request.body {
133 builder = builder
134 .header(CONTENT_TYPE, "application/json")
135 .body(body.clone());
136 }
137 let held = self.client.consistency_token();
138 if !held.is_empty() {
139 builder = builder.header(CONSISTENCY_TOKEN_HEADER, held);
140 }
141 let mut response = builder.send().await.map_err(Error::Transport)?;
142 if let Some(answered) = response
145 .headers()
146 .get(CONSISTENCY_TOKEN_HEADER)
147 .and_then(|v: &HeaderValue| v.to_str().ok())
148 .filter(|v| !v.is_empty())
149 {
150 self.client.set_consistency_token(answered);
151 }
152 let status = response.status();
153 let mut answer = Vec::new();
154 while let Some(chunk) = response.chunk().await.map_err(Error::Transport)? {
155 answer.extend_from_slice(&chunk);
156 if answer.len() >= MAX_RESPONSE_BYTES {
157 answer.truncate(MAX_RESPONSE_BYTES);
158 break;
159 }
160 }
161 if status != reqwest::StatusCode::OK {
162 return Err(ApiError::from_response(status.as_u16(), &answer).into());
163 }
164 Ok(answer)
165 }
166}
167
168pub(crate) fn is_timeout(err: &Error) -> bool {
170 matches!(err, Error::Transport(e) if e.is_timeout())
171}
172
173fn retry_delay(err: &Error, attempt: u32) -> Option<Duration> {
177 let asked = match err {
178 Error::Transport(_) => Duration::ZERO,
179 Error::Api(e) if e.is_retryable() => e.retry_delay.unwrap_or_default(),
180 _ => return None,
181 };
182 let backoff = FIRST_RETRY_DELAY
183 .checked_mul(1 << attempt.saturating_sub(1).min(16))
184 .map_or(MAX_RETRY_DELAY, |b| b.min(MAX_RETRY_DELAY));
185 let half = backoff / 2;
186 let jitter = Duration::from_nanos(fastrand::u64(
187 0..=u64::try_from(half.as_nanos()).unwrap_or(u64::MAX),
188 ));
189 Some(asked.max(half + jitter))
190}
191
192const PATH_SEGMENT: &AsciiSet = &NON_ALPHANUMERIC
195 .remove(b'-')
196 .remove(b'_')
197 .remove(b'.')
198 .remove(b'~')
199 .remove(b'$')
200 .remove(b'&')
201 .remove(b'+')
202 .remove(b':')
203 .remove(b'=')
204 .remove(b'@');
205
206pub fn path_value(field: &str, value: &str, pattern: &str) -> Result<String, Error> {
210 let got: Vec<&str> = value.split('/').collect();
211 let want: Vec<&str> = pattern.split('/').collect();
212 let valid = got.len() == want.len()
213 && got.iter().zip(&want).all(|(g, w)| match *w {
214 "*" => !g.is_empty() && *g != "." && *g != "..",
217 literal => *g == literal,
218 });
219 if !valid {
220 return Err(Error::InvalidRequest(format!(
221 "{field} must have the form {pattern}, got {value:?}"
222 )));
223 }
224 Ok(got
225 .iter()
226 .map(|segment| utf8_percent_encode(segment, PATH_SEGMENT).to_string())
227 .collect::<Vec<_>>()
228 .join("/"))
229}
230
231pub fn query<T: Serialize>(message: &T, skip: &[&str]) -> Result<Query, Error> {
237 let encoded = serde_json::to_value(message)
238 .map_err(|e| Error::InvalidRequest(format!("encoding the request: {e}")))?;
239 let mut query = Query::new();
240 if let Value::Object(fields) = encoded {
241 flatten(&mut query, "", &fields, skip)?;
242 }
243 query.sort_by(|a, b| a.0.cmp(&b.0));
244 Ok(query)
245}
246
247fn flatten(
248 query: &mut Query,
249 prefix: &str,
250 fields: &serde_json::Map<String, Value>,
251 skip: &[&str],
252) -> Result<(), Error> {
253 for (name, value) in fields {
254 let path = format!("{prefix}{name}");
255 if skip.contains(&path.as_str()) {
256 continue;
257 }
258 match value {
259 Value::Object(nested) => flatten(query, &format!("{path}."), nested, skip)?,
260 Value::Array(elements) => {
261 for element in elements {
262 query.push((path.clone(), scalar(&path, element)?));
263 }
264 }
265 other => query.push((path.clone(), scalar(&path, other)?)),
266 }
267 }
268 Ok(())
269}
270
271fn scalar(path: &str, value: &Value) -> Result<String, Error> {
272 match value {
273 Value::String(s) => Ok(s.clone()),
274 Value::Number(n) => Ok(n.to_string()),
275 Value::Bool(b) => Ok(b.to_string()),
276 _ => Err(Error::InvalidRequest(format!(
277 "{path} can't be sent as a query parameter"
278 ))),
279 }
280}
281
282pub fn json_body<T: Serialize>(message: &T) -> Result<Vec<u8>, Error> {
284 serde_json::to_vec(message)
285 .map_err(|e| Error::InvalidRequest(format!("encoding the request: {e}")))
286}
287
288pub fn new_request_id() -> String {
291 uuid::Uuid::new_v4().to_string()
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn a_path_value_is_checked_and_escaped() {
300 assert_eq!(
301 path_value(
302 "name",
303 "projects/p 1/zones/z/volumes/vol-1",
304 "projects/*/zones/*/volumes/*"
305 )
306 .unwrap(),
307 "projects/p%201/zones/z/volumes/vol-1"
308 );
309 assert_eq!(
310 path_value(
311 "name",
312 "projects/a b%/zones/z/volumes/v",
313 "projects/*/zones/*/volumes/*"
314 )
315 .unwrap(),
316 "projects/a%20b%25/zones/z/volumes/v"
317 );
318 for bad in [
319 "",
320 "projects/p/zones/z",
321 "projects/p/zones//volumes/v",
322 "projects/p/regions/z/volumes/v",
323 "projects/p/zones/z/volumes/v/x",
324 "projects/p/zones/z/volumes/..",
325 "projects/p/zones/z/volumes/.",
326 "projects/../zones/z/volumes/v",
327 ] {
328 let err = path_value("name", bad, "projects/*/zones/*/volumes/*").unwrap_err();
329 assert!(
330 err.to_string()
331 .contains("name must have the form projects/*/zones/*/volumes/*"),
332 "{bad:?}: {err}"
333 );
334 }
335 }
336
337 #[test]
338 fn query_flattens_by_json_path() {
339 let message = serde_json::json!({
340 "parent": "projects/p/zones/z",
341 "pageSize": 10,
342 "filter": {"state": "AVAILABLE", "ids": ["a", "b"]},
343 "all": true,
344 });
345 let got = query(&message, &["parent"]).unwrap();
346 let encoded: Vec<String> = got.iter().map(|(k, v)| format!("{k}={v}")).collect();
347 assert_eq!(
348 encoded.join("&"),
349 "all=true&filter.ids=a&filter.ids=b&filter.state=AVAILABLE&pageSize=10"
350 );
351 let err = query(&serde_json::json!({"items": [{"a": 1}]}), &[]).unwrap_err();
352 assert!(
353 err.to_string()
354 .contains("items can't be sent as a query parameter")
355 );
356 }
357
358 #[test]
359 fn new_request_id_is_a_version_4_uuid() {
360 let mut seen = std::collections::HashSet::new();
361 for _ in 0..100 {
362 let id = new_request_id();
363 let parsed = uuid::Uuid::parse_str(&id).unwrap();
364 assert_eq!(parsed.get_version_num(), 4);
365 assert_eq!(id, id.to_lowercase());
366 assert!(seen.insert(id));
367 }
368 }
369
370 #[test]
371 fn the_retry_delay_grows_and_honours_the_answer() {
372 let unavailable = Error::from(ApiError::from_response(
373 503,
374 br#"{"error":{"status":"UNAVAILABLE","details":[{"@type":"type.googleapis.com/google.rpc.RetryInfo","retryDelay":"3s"}]}}"#,
375 ));
376 for attempt in 1..=10 {
377 let delay = retry_delay(&unavailable, attempt).unwrap();
378 assert!(
379 delay >= Duration::from_secs(3),
380 "attempt {attempt}: {delay:?}"
381 );
382 assert!(delay <= MAX_RETRY_DELAY, "attempt {attempt}: {delay:?}");
383 }
384 let proxy = Error::from(ApiError::from_response(502, b""));
385 let first = retry_delay(&proxy, 1).unwrap();
386 assert!(
387 first >= Duration::from_millis(100) && first <= Duration::from_millis(200),
388 "{first:?}"
389 );
390 let late = retry_delay(&proxy, 30).unwrap();
391 assert!(
392 late >= Duration::from_secs(5) && late <= MAX_RETRY_DELAY,
393 "{late:?}"
394 );
395 let refused = Error::from(ApiError::from_response(400, b""));
396 assert_eq!(retry_delay(&refused, 1), None);
397 assert_eq!(retry_delay(&Error::NoCredentials, 1), None);
398 }
399}