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 let detail = parsed_value
330 .as_ref()
331 .and_then(|v| v.get("detail"))
332 .and_then(Value::as_str);
333 Error::RateLimit {
334 retry_after: u32::try_from(retry_after).unwrap_or(u32::MAX),
335 limit_type: headers
336 .get(RATE_LIMIT_TYPE_HDR)
337 .and_then(|v| v.to_str().ok())
338 .filter(|s| !s.is_empty())
339 .map(str::to_string)
340 .or_else(|| detail.and_then(limit_type_from_detail).map(str::to_string)),
341 response,
342 }
343 }
344 code => Error::Api {
345 status: code,
346 message: body_message
347 .unwrap_or_else(|| format!("API request failed with status {code}")),
348 response,
349 },
350 }
351}
352
353fn limit_type_from_detail(detail: &str) -> Option<&'static str> {
357 const MARKER: &str = "exceeded for ";
358 let lower = detail.to_lowercase();
359 let is_name_char = |c: char| c.is_alphanumeric() || c == '_' || c == '-';
360 let name = lower.find(MARKER).and_then(|at| {
361 let rest = &lower[at + MARKER.len()..];
362 let end = rest.find(|c: char| !is_name_char(c)).unwrap_or(rest.len());
363 (end > 0).then(|| &rest[..end])
364 });
365 let text = name.unwrap_or(&lower);
366 if text.contains("daily") || contains_word(text, "day") {
367 Some("daily")
368 } else if text.contains("burst") || text.contains("minute") {
369 Some("burst")
370 } else {
371 None
372 }
373}
374
375fn contains_word(text: &str, word: &str) -> bool {
376 let is_word_char = |c: char| c.is_alphanumeric() || c == '_';
377 text.match_indices(word).any(|(at, _)| {
378 let before = text[..at].chars().next_back();
379 let after = text[at + word.len()..].chars().next();
380 !before.is_some_and(is_word_char) && !after.is_some_and(is_word_char)
381 })
382}
383
384fn extract_reason(v: &Value) -> Option<String> {
388 let Value::Object(map) = v else { return None };
389 let envelope_reason = |map: &serde_json::Map<String, Value>| {
390 ["error", "message", "detail"]
391 .iter()
392 .find_map(|key| match map.get(*key) {
393 Some(Value::String(s)) if !s.is_empty() => Some(s.clone()),
394 _ => None,
395 })
396 };
397 envelope_reason(map).or_else(|| match map.get("detail") {
398 Some(Value::Object(nested)) => envelope_reason(nested),
399 _ => None,
400 })
401}
402
403pub(crate) fn decode_json<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
406 serde_json::from_slice(bytes).map_err(Error::Decode)
407}
408
409pub(crate) fn decode_json_or_default<T>(bytes: &[u8]) -> Result<T>
412where
413 T: DeserializeOwned + Default,
414{
415 if bytes.is_empty() {
416 return Ok(T::default());
417 }
418 decode_json(bytes)
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use serde_json::json;
425
426 #[test]
427 fn extract_envelope_detail() {
428 let body = json!({"detail": "no soup for you"});
429 assert_eq!(
430 extract_validation_message(Some(&body)),
431 "invalid request parameters: no soup for you"
432 );
433 }
434
435 #[test]
436 fn extract_envelope_message() {
437 let body = json!({"message": "bad input"});
438 assert_eq!(
439 extract_validation_message(Some(&body)),
440 "invalid request parameters: bad input"
441 );
442 }
443
444 #[test]
445 fn extract_field_errors_sorted() {
446 let body = json!({
447 "zebra": ["last alphabetically"],
448 "apple": ["first alphabetically"],
449 });
450 assert_eq!(
452 extract_validation_message(Some(&body)),
453 "invalid request parameters: first alphabetically"
454 );
455 }
456
457 #[test]
458 fn extract_falls_back() {
459 let body = json!({});
460 assert_eq!(
461 extract_validation_message(Some(&body)),
462 "invalid request parameters"
463 );
464 assert_eq!(
465 extract_validation_message(None),
466 "invalid request parameters"
467 );
468 }
469
470 #[test]
471 fn extract_prefers_envelope_over_field() {
472 let body = json!({
473 "detail": "envelope wins",
474 "apple": ["field loses"],
475 });
476 assert_eq!(
477 extract_validation_message(Some(&body)),
478 "invalid request parameters: envelope wins"
479 );
480 }
481
482 #[test]
483 fn extract_string_field_error() {
484 let body = json!({"piid": "must be present"});
485 assert_eq!(
486 extract_validation_message(Some(&body)),
487 "invalid request parameters: must be present"
488 );
489 }
490
491 #[test]
492 fn limit_type_reads_the_rate_name_not_the_wait() {
493 let cases = [
494 (
495 "Rate limit exceeded for daily. Please try again in 16 hours, 58 minutes.",
496 Some("daily"),
497 ),
498 (
499 "Rate limit exceeded for daily. Please try again in 1 minute.",
500 Some("daily"),
501 ),
502 (
503 "Rate limit exceeded for burst. Please try again in 45 seconds.",
504 Some("burst"),
505 ),
506 (
507 "Rate limit exceeded for burst. Please try again in 1 minute.",
508 Some("burst"),
509 ),
510 (
511 "Rate limit exceeded for premium_daily. Please try again in 3 hours.",
512 Some("daily"),
513 ),
514 (
515 "Rate limit exceeded for burst. Please try again in 1 day.",
516 Some("burst"),
517 ),
518 ("Too many requests.", None),
519 ("", None),
520 ];
521 for (detail, expected) in cases {
522 assert_eq!(limit_type_from_detail(detail), expected, "{detail}");
523 }
524 }
525
526 fn rate_limit_type(headers: &HeaderMap, body: &[u8]) -> Option<String> {
527 match decode_error(StatusCode::TOO_MANY_REQUESTS, headers, body) {
528 Error::RateLimit { limit_type, .. } => limit_type,
529 other => panic!("expected Error::RateLimit, got {other:?}"),
530 }
531 }
532
533 #[test]
534 fn rate_limit_type_from_body_detail() {
535 let body = br#"{"detail": "Rate limit exceeded for daily. Please try again in 16 hours, 58 minutes.", "wait_in_seconds": 61080}"#;
536 assert_eq!(
537 rate_limit_type(&HeaderMap::new(), body).as_deref(),
538 Some("daily")
539 );
540 }
541
542 #[test]
543 fn rate_limit_type_none_without_a_recognisable_detail() {
544 let h = HeaderMap::new();
545 assert_eq!(
546 rate_limit_type(&h, br#"{"detail": "Too many requests."}"#),
547 None
548 );
549 assert_eq!(
550 rate_limit_type(&h, br#"{"detail": {"error": "daily"}}"#),
551 None
552 );
553 assert_eq!(rate_limit_type(&h, b"<html>daily burst</html>"), None);
554 assert_eq!(rate_limit_type(&h, b""), None);
555 }
556
557 #[test]
558 fn rate_limit_type_header_wins_over_detail() {
559 let body = br#"{"detail": "Rate limit exceeded for daily. Please try again in 2 hours."}"#;
560 let mut h = HeaderMap::new();
561 h.insert(RATE_LIMIT_TYPE_HDR, "burst".parse().unwrap());
562 assert_eq!(rate_limit_type(&h, body).as_deref(), Some("burst"));
563 h.insert(RATE_LIMIT_TYPE_HDR, "".parse().unwrap());
564 assert_eq!(rate_limit_type(&h, body).as_deref(), Some("daily"));
565 }
566
567 fn api_error(status: u16, body: &[u8]) -> (u16, String, Option<ErrorBody>, String) {
568 let err = decode_error(
569 StatusCode::from_u16(status).unwrap(),
570 &HeaderMap::new(),
571 body,
572 );
573 let display = err.to_string();
574 match err {
575 Error::Api {
576 status,
577 message,
578 response,
579 } => (status, message, response, display),
580 other => panic!("expected Error::Api, got {other:?}"),
581 }
582 }
583
584 #[test]
585 fn api_error_carries_the_server_reason() {
586 let body = json!({"detail": "These shape fields are not allowed for your account."});
587 let (status, message, response, display) = api_error(403, body.to_string().as_bytes());
588 assert_eq!(status, 403);
589 assert_eq!(
590 message,
591 "These shape fields are not allowed for your account."
592 );
593 assert_eq!(
594 display,
595 "tango: API error (status 403): These shape fields are not allowed for your account."
596 );
597 let response = response.unwrap();
598 assert_eq!(response.message, message);
599 assert_eq!(response.raw, Some(body));
600 }
601
602 #[test]
603 fn api_error_tier_gate_reason_flat_or_nested() {
604 let reason = "The 'psc' filter requires a Small plan or higher.";
605 for body in [
606 json!({"error": reason, "code": "tier_required"}),
607 json!({"detail": {"error": reason, "code": "tier_required"}}),
608 ] {
609 let (_, message, response, _) = api_error(403, body.to_string().as_bytes());
610 assert_eq!(message, reason, "body: {body}");
611 assert_eq!(response.unwrap().raw, Some(body));
612 }
613 }
614
615 #[test]
616 fn api_error_json_body_without_a_reason_keeps_the_body() {
617 let body = json!({"code": "forbidden"});
618 let (_, message, response, _) = api_error(403, body.to_string().as_bytes());
619 assert_eq!(message, "API request failed with status 403");
620 let response = response.unwrap();
621 assert_eq!(response.message, "");
622 assert_eq!(response.raw, Some(body));
623 }
624
625 #[test]
626 fn api_error_non_json_body_keeps_the_bare_message() {
627 let (status, message, response, _) = api_error(502, b"<html>Bad Gateway</html>");
628 assert_eq!(status, 502);
629 assert_eq!(message, "API request failed with status 502");
630 assert_eq!(response, None);
631 }
632
633 #[test]
634 fn backoff_doubles_and_caps() {
635 let base = Duration::from_millis(250);
636 assert_eq!(backoff_for(base, 0), Duration::from_millis(250));
637 assert_eq!(backoff_for(base, 1), Duration::from_millis(500));
638 assert_eq!(backoff_for(base, 2), Duration::from_secs(1));
639 assert_eq!(backoff_for(base, 3), Duration::from_secs(2));
640 assert_eq!(backoff_for(base, 4), Duration::from_secs(4));
641 assert_eq!(backoff_for(base, 5), Duration::from_secs(8));
642 assert_eq!(backoff_for(base, 6), MAX_BACKOFF);
643 assert_eq!(backoff_for(base, 50), MAX_BACKOFF);
644 }
645}