1use crate::error::{Error, ErrorBody, Result};
9use reqwest::{header::HeaderMap, Method, RequestBuilder, Response, StatusCode};
10use serde::de::DeserializeOwned;
11use serde_json::Value;
12use std::time::Duration;
13
14pub(crate) const MAX_BACKOFF: Duration = Duration::from_secs(10);
15
16pub(crate) const API_KEY_HEADER: &str = "X-API-KEY";
17pub(crate) const RATE_LIMIT_REMAINING_HDR: &str = "x-ratelimit-remaining";
18pub(crate) const RATE_LIMIT_LIMIT_HDR: &str = "x-ratelimit-limit";
19pub(crate) const RATE_LIMIT_RESET_HDR: &str = "x-ratelimit-reset";
20pub(crate) const RATE_LIMIT_TYPE_HDR: &str = "x-ratelimit-type";
21pub(crate) const RETRY_AFTER_HDR: &str = "retry-after";
22
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
29pub struct RateLimitInfo {
30 pub remaining: Option<i64>,
32 pub limit: Option<i64>,
34 pub reset_in: Option<i64>,
36 pub retry_after: Option<i64>,
38 pub limit_type: Option<String>,
40}
41
42impl RateLimitInfo {
43 pub(crate) fn from_headers(h: &HeaderMap) -> Self {
44 Self {
45 remaining: parse_int_header(h, RATE_LIMIT_REMAINING_HDR),
46 limit: parse_int_header(h, RATE_LIMIT_LIMIT_HDR),
47 reset_in: parse_int_header(h, RATE_LIMIT_RESET_HDR),
48 retry_after: parse_int_header(h, RETRY_AFTER_HDR),
49 limit_type: h
50 .get(RATE_LIMIT_TYPE_HDR)
51 .and_then(|v| v.to_str().ok())
52 .map(str::to_string),
53 }
54 }
55}
56
57fn parse_int_header(h: &HeaderMap, key: &str) -> Option<i64> {
58 h.get(key)
59 .and_then(|v| v.to_str().ok())
60 .map(str::trim)
61 .and_then(|s| s.parse::<i64>().ok())
62}
63
64pub(crate) fn parse_retry_after(h: &HeaderMap) -> Duration {
68 let Some(raw) = h.get(RETRY_AFTER_HDR).and_then(|v| v.to_str().ok()) else {
69 return Duration::ZERO;
70 };
71 let raw = raw.trim();
72 if let Ok(secs) = raw.parse::<u64>() {
73 return Duration::from_secs(secs).min(MAX_BACKOFF);
74 }
75 Duration::ZERO
79}
80
81pub(crate) fn extract_validation_message(body: Option<&Value>) -> String {
88 const FALLBACK: &str = "invalid request parameters";
89 let Some(Value::Object(map)) = body else {
90 return FALLBACK.to_string();
91 };
92 for key in ["detail", "message", "error"] {
93 if let Some(Value::String(s)) = map.get(key) {
94 if !s.is_empty() {
95 return format!("invalid request parameters: {s}");
96 }
97 }
98 }
99 let mut keys: Vec<&String> = map.keys().collect();
101 keys.sort();
102 for k in keys {
103 match map.get(k) {
104 Some(Value::Array(arr)) if !arr.is_empty() => {
105 if let Some(Value::String(s)) = arr.first() {
106 if !s.is_empty() {
107 return format!("invalid request parameters: {s}");
108 }
109 }
110 }
111 Some(Value::String(s)) if !s.is_empty() => {
112 return format!("invalid request parameters: {s}");
113 }
114 _ => {}
115 }
116 }
117 FALLBACK.to_string()
118}
119
120#[derive(Debug)]
123pub(crate) enum Body<'a> {
124 None,
125 Json(&'a Value),
126}
127
128pub(crate) async fn send_with_retries(
129 inner: &crate::client::ClientInner,
130 method: Method,
131 url: reqwest::Url,
132 body: Body<'_>,
133) -> Result<Vec<u8>> {
134 send_with_retries_and_headers(inner, method, url, body)
135 .await
136 .map(|(bytes, _)| bytes)
137}
138
139pub(crate) async fn send_with_retries_and_headers(
141 inner: &crate::client::ClientInner,
142 method: Method,
143 url: reqwest::Url,
144 body: Body<'_>,
145) -> Result<(Vec<u8>, HeaderMap)> {
146 let body_bytes = match body {
148 Body::None => None,
149 Body::Json(v) => Some(serde_json::to_vec(v)?),
150 };
151 retry(inner, || {
152 attempt_once(inner, method.clone(), url.clone(), body_bytes.as_deref())
153 })
154 .await
155}
156
157pub(crate) async fn redirect_location_with_retries(
160 inner: &crate::client::ClientInner,
161 url: reqwest::Url,
162) -> Result<String> {
163 retry(inner, || attempt_redirect(inner, url.clone())).await
164}
165
166async fn retry<T, F, Fut>(inner: &crate::client::ClientInner, mut op: F) -> Result<T>
167where
168 F: FnMut() -> Fut,
169 Fut: std::future::Future<Output = Result<T>>,
170{
171 let max_attempts = inner.retries.saturating_add(1);
172 let mut attempt: u32 = 0;
173
174 loop {
175 let err = match op().await {
176 Ok(v) => return Ok(v),
177 Err(e) => e,
178 };
179
180 if !err.is_retryable() || attempt + 1 >= max_attempts {
181 return Err(err);
182 }
183
184 let wait = if let Error::RateLimit { retry_after, .. } = &err {
185 let r = u64::from(*retry_after);
186 if r > 0 {
187 Duration::from_secs(r).min(MAX_BACKOFF)
188 } else {
189 backoff_for(inner.retry_backoff, attempt)
190 }
191 } else {
192 backoff_for(inner.retry_backoff, attempt)
193 };
194
195 tokio::time::sleep(wait).await;
196 attempt += 1;
197 }
198}
199
200fn backoff_for(base: Duration, attempt: u32) -> Duration {
201 let mult = 1u32 << attempt.min(6); base.saturating_mul(mult).min(MAX_BACKOFF)
203}
204
205fn build_request(
206 inner: &crate::client::ClientInner,
207 http: &reqwest::Client,
208 method: Method,
209 url: reqwest::Url,
210 body_bytes: Option<&[u8]>,
211) -> RequestBuilder {
212 let mut req: RequestBuilder = http.request(method, url);
213 req = req.header(reqwest::header::ACCEPT, "application/json");
214 if !inner.api_key.is_empty() {
215 req = req.header(API_KEY_HEADER, &inner.api_key);
216 }
217 if !inner.user_agent.is_empty() {
218 req = req.header(reqwest::header::USER_AGENT, &inner.user_agent);
219 }
220 if let Some(bytes) = body_bytes {
221 req = req
222 .header(reqwest::header::CONTENT_TYPE, "application/json")
223 .body(bytes.to_vec());
224 }
225 if !inner.timeout.is_zero() {
226 req = req.timeout(inner.timeout);
227 }
228 req
229}
230
231async fn send_raw(
234 inner: &crate::client::ClientInner,
235 req: RequestBuilder,
236) -> Result<(StatusCode, HeaderMap, Vec<u8>)> {
237 let resp: Response = match req.send().await {
238 Ok(r) => r,
239 Err(e) => {
240 if e.is_timeout() {
241 return Err(Error::Timeout {
242 timeout: inner.timeout,
243 });
244 }
245 return Err(Error::Transport(e));
246 }
247 };
248
249 let status = resp.status();
250 let headers = resp.headers().clone();
251 inner.set_last_response(&headers);
253
254 let bytes = match resp.bytes().await {
255 Ok(b) => b.to_vec(),
256 Err(e) => return Err(Error::Transport(e)),
257 };
258 Ok((status, headers, bytes))
259}
260
261async fn attempt_once(
262 inner: &crate::client::ClientInner,
263 method: Method,
264 url: reqwest::Url,
265 body_bytes: Option<&[u8]>,
266) -> Result<(Vec<u8>, HeaderMap)> {
267 let req = build_request(inner, &inner.http, method, url, body_bytes);
268 let (status, headers, bytes) = send_raw(inner, req).await?;
269 if status.is_success() {
270 return Ok((bytes, headers));
271 }
272 Err(decode_error(status, &headers, &bytes))
273}
274
275async fn attempt_redirect(inner: &crate::client::ClientInner, url: reqwest::Url) -> Result<String> {
276 let req = build_request(
277 inner,
278 &inner.http_no_redirect,
279 Method::GET,
280 url.clone(),
281 None,
282 );
283 let (status, headers, bytes) = send_raw(inner, req).await?;
284 if status.is_redirection() {
285 let location = headers
286 .get(reqwest::header::LOCATION)
287 .and_then(|v| v.to_str().ok())
288 .filter(|s| !s.is_empty())
289 .ok_or_else(|| Error::Api {
290 status: status.as_u16(),
291 message: "redirect response carried no Location header".into(),
292 response: None,
293 })?;
294 return url
295 .join(location)
296 .map(String::from)
297 .map_err(|e| Error::Build(format!("parse redirect location {location}: {e}")));
298 }
299 if status.is_success() {
300 return Err(Error::Api {
301 status: status.as_u16(),
302 message: format!("expected a redirect, got status {}", status.as_u16()),
303 response: None,
304 });
305 }
306 Err(decode_error(status, &headers, &bytes))
307}
308
309fn decode_error(status: StatusCode, headers: &HeaderMap, body: &[u8]) -> Error {
310 let parsed_value: Option<Value> = if body.is_empty() {
311 None
312 } else {
313 serde_json::from_slice(body).ok()
314 };
315 let body_message = parsed_value.as_ref().and_then(extract_reason);
316 let response = parsed_value.as_ref().map(|v| ErrorBody {
317 message: body_message.clone().unwrap_or_default(),
318 raw: Some(v.clone()),
319 });
320 match status.as_u16() {
321 401 => Error::Auth { response },
322 404 => Error::NotFound { response },
323 400 => Error::Validation {
324 message: extract_validation_message(parsed_value.as_ref()),
325 response,
326 },
327 429 => {
328 let retry_after = parse_retry_after(headers).as_secs();
329 Error::RateLimit {
330 retry_after: u32::try_from(retry_after).unwrap_or(u32::MAX),
331 limit_type: headers
332 .get(RATE_LIMIT_TYPE_HDR)
333 .and_then(|v| v.to_str().ok())
334 .map(str::to_string),
335 response,
336 }
337 }
338 code => Error::Api {
339 status: code,
340 message: body_message
341 .unwrap_or_else(|| format!("API request failed with status {code}")),
342 response,
343 },
344 }
345}
346
347fn extract_reason(v: &Value) -> Option<String> {
351 let Value::Object(map) = v else { return None };
352 let envelope_reason = |map: &serde_json::Map<String, Value>| {
353 ["error", "message", "detail"]
354 .iter()
355 .find_map(|key| match map.get(*key) {
356 Some(Value::String(s)) if !s.is_empty() => Some(s.clone()),
357 _ => None,
358 })
359 };
360 envelope_reason(map).or_else(|| match map.get("detail") {
361 Some(Value::Object(nested)) => envelope_reason(nested),
362 _ => None,
363 })
364}
365
366pub(crate) fn decode_json<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
369 serde_json::from_slice(bytes).map_err(Error::Decode)
370}
371
372pub(crate) fn decode_json_or_default<T>(bytes: &[u8]) -> Result<T>
375where
376 T: DeserializeOwned + Default,
377{
378 if bytes.is_empty() {
379 return Ok(T::default());
380 }
381 decode_json(bytes)
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use serde_json::json;
388
389 #[test]
390 fn extract_envelope_detail() {
391 let body = json!({"detail": "no soup for you"});
392 assert_eq!(
393 extract_validation_message(Some(&body)),
394 "invalid request parameters: no soup for you"
395 );
396 }
397
398 #[test]
399 fn extract_envelope_message() {
400 let body = json!({"message": "bad input"});
401 assert_eq!(
402 extract_validation_message(Some(&body)),
403 "invalid request parameters: bad input"
404 );
405 }
406
407 #[test]
408 fn extract_field_errors_sorted() {
409 let body = json!({
410 "zebra": ["last alphabetically"],
411 "apple": ["first alphabetically"],
412 });
413 assert_eq!(
415 extract_validation_message(Some(&body)),
416 "invalid request parameters: first alphabetically"
417 );
418 }
419
420 #[test]
421 fn extract_falls_back() {
422 let body = json!({});
423 assert_eq!(
424 extract_validation_message(Some(&body)),
425 "invalid request parameters"
426 );
427 assert_eq!(
428 extract_validation_message(None),
429 "invalid request parameters"
430 );
431 }
432
433 #[test]
434 fn extract_prefers_envelope_over_field() {
435 let body = json!({
436 "detail": "envelope wins",
437 "apple": ["field loses"],
438 });
439 assert_eq!(
440 extract_validation_message(Some(&body)),
441 "invalid request parameters: envelope wins"
442 );
443 }
444
445 #[test]
446 fn extract_string_field_error() {
447 let body = json!({"piid": "must be present"});
448 assert_eq!(
449 extract_validation_message(Some(&body)),
450 "invalid request parameters: must be present"
451 );
452 }
453
454 fn api_error(status: u16, body: &[u8]) -> (u16, String, Option<ErrorBody>, String) {
455 let err = decode_error(
456 StatusCode::from_u16(status).unwrap(),
457 &HeaderMap::new(),
458 body,
459 );
460 let display = err.to_string();
461 match err {
462 Error::Api {
463 status,
464 message,
465 response,
466 } => (status, message, response, display),
467 other => panic!("expected Error::Api, got {other:?}"),
468 }
469 }
470
471 #[test]
472 fn api_error_carries_the_server_reason() {
473 let body = json!({"detail": "These shape fields are not allowed for your account."});
474 let (status, message, response, display) = api_error(403, body.to_string().as_bytes());
475 assert_eq!(status, 403);
476 assert_eq!(
477 message,
478 "These shape fields are not allowed for your account."
479 );
480 assert_eq!(
481 display,
482 "tango: API error (status 403): These shape fields are not allowed for your account."
483 );
484 let response = response.unwrap();
485 assert_eq!(response.message, message);
486 assert_eq!(response.raw, Some(body));
487 }
488
489 #[test]
490 fn api_error_tier_gate_reason_flat_or_nested() {
491 let reason = "The 'psc' filter requires a Small plan or higher.";
492 for body in [
493 json!({"error": reason, "code": "tier_required"}),
494 json!({"detail": {"error": reason, "code": "tier_required"}}),
495 ] {
496 let (_, message, response, _) = api_error(403, body.to_string().as_bytes());
497 assert_eq!(message, reason, "body: {body}");
498 assert_eq!(response.unwrap().raw, Some(body));
499 }
500 }
501
502 #[test]
503 fn api_error_json_body_without_a_reason_keeps_the_body() {
504 let body = json!({"code": "forbidden"});
505 let (_, message, response, _) = api_error(403, body.to_string().as_bytes());
506 assert_eq!(message, "API request failed with status 403");
507 let response = response.unwrap();
508 assert_eq!(response.message, "");
509 assert_eq!(response.raw, Some(body));
510 }
511
512 #[test]
513 fn api_error_non_json_body_keeps_the_bare_message() {
514 let (status, message, response, _) = api_error(502, b"<html>Bad Gateway</html>");
515 assert_eq!(status, 502);
516 assert_eq!(message, "API request failed with status 502");
517 assert_eq!(response, None);
518 }
519
520 #[test]
521 fn backoff_doubles_and_caps() {
522 let base = Duration::from_millis(250);
523 assert_eq!(backoff_for(base, 0), Duration::from_millis(250));
524 assert_eq!(backoff_for(base, 1), Duration::from_millis(500));
525 assert_eq!(backoff_for(base, 2), Duration::from_secs(1));
526 assert_eq!(backoff_for(base, 3), Duration::from_secs(2));
527 assert_eq!(backoff_for(base, 4), Duration::from_secs(4));
528 assert_eq!(backoff_for(base, 5), Duration::from_secs(8));
529 assert_eq!(backoff_for(base, 6), MAX_BACKOFF);
530 assert_eq!(backoff_for(base, 50), MAX_BACKOFF);
531 }
532}