1use std::{
37 sync::{Arc, OnceLock},
38 time::Duration,
39};
40
41use reqwest::Method;
42use serde::{Deserialize, de::DeserializeOwned};
43use tracing::{trace, warn};
44
45use crate::client::error::{ZaiError, ZaiResult, mask_sensitive_info};
46
47#[derive(Debug, Deserialize)]
48#[serde(untagged)]
49enum ApiErrorEnvelope {
50 Nested { error: ApiError },
51 Flat { code: ErrorCode, message: String },
52}
53
54impl ApiErrorEnvelope {
55 fn into_parts(self) -> (ErrorCode, String) {
56 match self {
57 ApiErrorEnvelope::Nested { error } => (error.code, error.message),
58 ApiErrorEnvelope::Flat { code, message } => (code, message),
59 }
60 }
61}
62
63#[derive(Debug, Deserialize)]
64
65struct ApiError {
66 code: ErrorCode,
67
68 message: String,
69}
70
71#[derive(Debug, Deserialize)]
72#[serde(untagged)]
73enum ErrorCode {
74 Str(String),
75
76 Num(i64),
77}
78
79impl std::fmt::Display for ErrorCode {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 ErrorCode::Str(s) => write!(f, "{}", s),
83
84 ErrorCode::Num(n) => write!(f, "{}", n),
85 }
86 }
87}
88
89fn to_api_code(code: &ErrorCode) -> u16 {
90 match code {
91 ErrorCode::Num(n) => (*n).try_into().unwrap_or(0),
92 ErrorCode::Str(s) => s.parse::<u16>().unwrap_or(0),
93 }
94}
95
96pub fn parse_api_error_response(status: u16, body: String) -> crate::client::error::ZaiError {
102 if let Ok(parsed) = serde_json::from_str::<ApiErrorEnvelope>(&body) {
103 let (code, message) = parsed.into_parts();
104 let api_code = to_api_code(&code);
105 crate::client::error::ZaiError::from_api_response(status, api_code, message)
106 } else {
107 crate::client::error::ZaiError::from_api_response(status, 0, body)
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum RetryDelay {
114 Fixed(Duration),
116
117 Exponential { base: Duration, max: Duration },
119
120 None,
122}
123
124impl RetryDelay {
125 pub fn fixed(delay: Duration) -> Self {
127 Self::Fixed(delay)
128 }
129
130 pub fn exponential(base: Duration, max: Duration) -> Self {
132 Self::Exponential { base, max }
133 }
134
135 pub fn none() -> Self {
137 Self::None
138 }
139}
140
141impl Default for RetryDelay {
142 fn default() -> Self {
143 Self::Exponential {
144 base: Duration::from_millis(500),
145 max: Duration::from_secs(5),
146 }
147 }
148}
149
150#[derive(Debug, Clone)]
165pub struct HttpClientConfig {
166 pub timeout: Duration,
168
169 pub max_retries: u32,
171
172 pub enable_compression: bool,
174
175 pub retry_delay: RetryDelay,
177
178 pub enable_logging: bool,
186
187 pub mask_sensitive_data: bool,
189}
190
191impl Default for HttpClientConfig {
192 fn default() -> Self {
193 Self {
194 timeout: Duration::from_secs(60),
195 max_retries: 3,
196 enable_compression: true,
197 retry_delay: RetryDelay::default(),
198 enable_logging: false,
199 mask_sensitive_data: true,
200 }
201 }
202}
203
204impl HttpClientConfig {
205 pub fn builder() -> HttpClientConfigBuilder {
207 HttpClientConfigBuilder::new()
208 }
209}
210
211pub struct HttpClientConfigBuilder {
227 config: HttpClientConfig,
228}
229
230impl HttpClientConfigBuilder {
231 pub fn new() -> Self {
233 Self {
234 config: HttpClientConfig::default(),
235 }
236 }
237
238 pub fn timeout(mut self, timeout: Duration) -> Self {
240 self.config.timeout = timeout;
241 self
242 }
243
244 pub fn max_retries(mut self, max_retries: u32) -> Self {
246 self.config.max_retries = max_retries;
247 self
248 }
249
250 pub fn compression(mut self, enable: bool) -> Self {
252 self.config.enable_compression = enable;
253 self
254 }
255
256 pub fn retry_delay(mut self, delay: RetryDelay) -> Self {
258 self.config.retry_delay = delay;
259 self
260 }
261
262 pub fn logging(mut self, enable: bool) -> Self {
265 self.config.enable_logging = enable;
266 self
267 }
268
269 pub fn mask_sensitive_data(mut self, enable: bool) -> Self {
271 self.config.mask_sensitive_data = enable;
272 self
273 }
274
275 pub fn build(self) -> HttpClientConfig {
277 self.config
278 }
279}
280
281impl Default for HttpClientConfigBuilder {
282 fn default() -> Self {
283 Self::new()
284 }
285}
286
287static HTTP_CLIENTS: OnceLock<dashmap::DashMap<String, reqwest::Client>> = OnceLock::new();
290
291pub fn http_client_with_config(config: &HttpClientConfig) -> reqwest::Client {
295 let config_key = format!(
296 "timeout:{:?}|compression:{}",
297 config.timeout, config.enable_compression
298 );
299
300 let clients = HTTP_CLIENTS.get_or_init(dashmap::DashMap::new);
301
302 clients
303 .entry(config_key)
304 .or_insert_with(|| {
305 let builder = reqwest::Client::builder().timeout(config.timeout);
306
307 builder.build().expect("Failed to build reqwest Client")
315 })
316 .clone()
317}
318
319#[tracing::instrument(
329 name = "http.response",
330 skip_all,
331 fields(
332 otel.name = "http.parse_response",
333 http.url = tracing::field::Empty,
334 http.status_code = tracing::field::Empty,
335 )
336)]
337pub async fn parse_typed_response<T>(resp: reqwest::Response) -> ZaiResult<T>
338where
339 T: DeserializeOwned,
340{
341 let status = resp.status();
342 let url = resp.url().to_string();
343
344 let span = tracing::Span::current();
345 span.record("http.url", url.as_str());
346 span.record("http.status_code", status.as_u16());
347
348 let body = resp.text().await.map_err(ZaiError::from)?;
349
350 trace!(
351 url = %url,
352 http_status = %status,
353 bytes = body.len(),
354 response_body = %mask_sensitive_info(&body),
355 "Received HTTP response body"
356 );
357
358 serde_json::from_str::<T>(&body).map_err(ZaiError::from)
359}
360
361#[tracing::instrument(
371 name = "http.request",
372 skip_all,
373 fields(
374 otel.name = "http.send_json",
375 http.method = %method,
376 http.url = tracing::field::Empty,
377 )
378)]
379pub async fn send_json_request<T>(
380 method: Method,
381 url: impl Into<String>,
382 api_key: impl AsRef<str>,
383 body: &T,
384 config: Arc<HttpClientConfig>,
385) -> ZaiResult<reqwest::Response>
386where
387 T: serde::Serialize + ?Sized,
388{
389 let body_compact = serde_json::to_string(body).map_err(|e| ZaiError::JsonError(Arc::new(e)))?;
390 let url_value: String = url.into();
391
392 tracing::Span::current().record("http.url", url_value.as_str());
393
394 trace!(
397 method = %method,
398 url = %url_value,
399 bytes = body_compact.len(),
400 request_body = %mask_sensitive_info(&body_compact),
401 "Sending HTTP request body"
402 );
403
404 let key = api_key.as_ref().to_string();
405 send_with_retry_factory(&config, move |client| {
406 let builder = client
407 .request(method.clone(), &url_value)
408 .bearer_auth(&key)
409 .header("Content-Type", "application/json")
410 .body(body_compact.clone());
411
412 Ok(builder)
413 })
414 .await
415}
416
417#[tracing::instrument(
422 name = "http.request",
423 skip_all,
424 fields(
425 otel.name = "http.send_empty",
426 http.method = %method,
427 http.url = tracing::field::Empty,
428 )
429)]
430pub async fn send_empty_request(
431 method: Method,
432 url: impl Into<String>,
433 api_key: impl AsRef<str>,
434 config: Arc<HttpClientConfig>,
435) -> ZaiResult<reqwest::Response> {
436 let url_value: String = url.into();
437 tracing::Span::current().record("http.url", url_value.as_str());
438 trace!(
439 method = %method,
440 url = %url_value,
441 request_body = %"",
442 "Sending HTTP request (no body)"
443 );
444 let key = api_key.as_ref().to_string();
445 send_with_retry_factory(&config, move |client| {
446 Ok(client.request(method.clone(), &url_value).bearer_auth(&key))
447 })
448 .await
449}
450
451#[tracing::instrument(
458 name = "http.request",
459 skip_all,
460 fields(
461 otel.name = "http.send_multipart",
462 http.method = %method,
463 http.url = tracing::field::Empty,
464 )
465)]
466pub async fn send_multipart_request<F>(
467 method: Method,
468 url: impl Into<String>,
469 api_key: impl AsRef<str>,
470 config: Arc<HttpClientConfig>,
471 mut build_form: F,
472) -> ZaiResult<reqwest::Response>
473where
474 F: FnMut() -> ZaiResult<reqwest::multipart::Form> + Send,
475{
476 let url_value: String = url.into();
477 tracing::Span::current().record("http.url", url_value.as_str());
478 trace!(
479 method = %method,
480 url = %url_value,
481 "Sending multipart HTTP request"
482 );
483 let key = api_key.as_ref().to_string();
484 send_with_retry_factory(&config, move |client| {
485 let form = build_form()?;
486 Ok(client
487 .request(method.clone(), &url_value)
488 .bearer_auth(&key)
489 .multipart(form))
490 })
491 .await
492}
493
494pub trait HttpClient {
496 type Body: serde::Serialize;
497 type ApiUrl: AsRef<str>;
498 type ApiKey: AsRef<str>;
499
500 fn api_url(&self) -> &Self::ApiUrl;
501 fn api_key(&self) -> &Self::ApiKey;
502 fn body(&self) -> &Self::Body;
503
504 fn http_config(&self) -> Arc<HttpClientConfig> {
509 static DEFAULT: std::sync::OnceLock<Arc<HttpClientConfig>> = std::sync::OnceLock::new();
510 DEFAULT
511 .get_or_init(|| Arc::new(HttpClientConfig::default()))
512 .clone()
513 }
514
515 fn post(&self) -> impl std::future::Future<Output = ZaiResult<reqwest::Response>> + Send {
520 let config = self.http_config().clone();
521 let url = self.api_url().as_ref().to_owned();
522 let key = self.api_key().as_ref().to_owned();
523 let body = serde_json::to_value(self.body()).map_err(|e| ZaiError::JsonError(Arc::new(e)));
524
525 async move {
526 let body = body?;
527 send_json_request(Method::POST, url, key, &body, config).await
528 }
529 }
530
531 fn get(&self) -> impl std::future::Future<Output = ZaiResult<reqwest::Response>> + Send {
536 let config = self.http_config().clone();
537 let url = self.api_url().as_ref().to_owned();
538 let key = self.api_key().as_ref().to_owned();
539
540 async move { send_empty_request(Method::GET, url, key, config).await }
541 }
542
543 fn put(&self) -> impl std::future::Future<Output = ZaiResult<reqwest::Response>> + Send {
545 let config = self.http_config().clone();
546 let url = self.api_url().as_ref().to_owned();
547 let key = self.api_key().as_ref().to_owned();
548 let body = serde_json::to_value(self.body()).map_err(|e| ZaiError::JsonError(Arc::new(e)));
549
550 async move {
551 let body = body?;
552 send_json_request(Method::PUT, url, key, &body, config).await
553 }
554 }
555
556 fn delete(&self) -> impl std::future::Future<Output = ZaiResult<reqwest::Response>> + Send {
558 let config = self.http_config().clone();
559 let url = self.api_url().as_ref().to_owned();
560 let key = self.api_key().as_ref().to_owned();
561
562 async move { send_empty_request(Method::DELETE, url, key, config).await }
563 }
564}
565
566#[tracing::instrument(
568 skip(config, build_request),
569 fields(max_retries = config.max_retries, attempt = tracing::field::Empty)
570)]
571async fn send_with_retry_factory<F>(
572 config: &HttpClientConfig,
573 mut build_request: F,
574) -> ZaiResult<reqwest::Response>
575where
576 F: FnMut(reqwest::Client) -> ZaiResult<reqwest::RequestBuilder>,
577{
578 let mut last_error: Option<ZaiError> = None;
579 let client = http_client_with_config(config);
580
581 for attempt in 0..=config.max_retries {
582 tracing::Span::current().record("attempt", attempt);
583 let resp = match build_request(client.clone()) {
584 Ok(builder) => builder.send().await,
585 Err(error) => return Err(error),
586 };
587
588 match resp {
589 Ok(resp) => {
590 let status = resp.status();
591 let url = resp.url().to_string();
592
593 if status.is_success() {
594 trace!(http_status = %status, url = %url, "Request succeeded");
595 return Ok(resp);
596 }
597
598 let text = resp.text().await.unwrap_or_default();
599 let error = parse_api_error_response(status.as_u16(), text);
600
601 if should_retry(&error, attempt, config.max_retries) {
602 last_error = Some(error.clone());
603 let delay = calculate_retry_delay(attempt, &config.retry_delay);
604 let delay_with_jitter = add_jitter(delay);
605 warn!(
606 url = %url,
607 http_status = %status,
608 attempt = attempt + 1,
609 max_attempts = config.max_retries + 1,
610 retry_delay = ?delay_with_jitter,
611 error = %error.compact(),
612 "Request failed, retrying"
613 );
614 tokio::time::sleep(delay_with_jitter).await;
615 } else {
616 return Err(error);
617 }
618 },
619 Err(e) => {
620 let url = e.url().map(|u| u.to_string()).unwrap_or_default();
621 let error = ZaiError::from(e);
622
623 if should_retry(&error, attempt, config.max_retries) {
624 last_error = Some(error.clone());
625 let delay = calculate_retry_delay(attempt, &config.retry_delay);
626 let delay_with_jitter = add_jitter(delay);
627 warn!(
628 url = %url,
629 attempt = attempt + 1,
630 max_attempts = config.max_retries + 1,
631 retry_delay = ?delay_with_jitter,
632 error = %error.compact(),
633 "Request failed, retrying"
634 );
635 tokio::time::sleep(delay_with_jitter).await;
636 } else {
637 return Err(error);
638 }
639 },
640 }
641 }
642
643 let final_err = last_error.unwrap_or_else(|| ZaiError::HttpError {
644 status: 500,
645 message: "Unknown error after retries".to_string(),
646 });
647 warn!(
648 code = ?final_err.code(),
649 error = %final_err.compact(),
650 "Request failed after all retries"
651 );
652 Err(final_err)
653}
654
655fn calculate_retry_delay(attempt: u32, strategy: &RetryDelay) -> Duration {
657 match strategy {
658 RetryDelay::Fixed(delay) => *delay,
659 RetryDelay::Exponential { base, max } => {
660 let delay = *base * 2u32.pow(attempt.min(10));
661 delay.min(*max)
662 },
663 RetryDelay::None => Duration::ZERO,
664 }
665}
666
667fn should_retry(error: &ZaiError, attempt: u32, max_retries: u32) -> bool {
669 if attempt >= max_retries {
670 return false;
671 }
672
673 match error {
674 ZaiError::HttpError { status, .. } => *status == 429 || (500..600).contains(status),
676 ZaiError::RateLimitError { .. } => true,
678 ZaiError::NetworkError(_) => true,
680 _ => false,
682 }
683}
684
685fn add_jitter(delay: Duration) -> Duration {
687 let jitter_ms = fastrand::u64(0..=delay.as_millis() as u64 / 4);
688 delay + Duration::from_millis(jitter_ms)
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694
695 #[test]
696 fn test_error_code_display_num() {
697 let code = ErrorCode::Num(123);
698 assert_eq!(format!("{}", code), "123");
699 }
700
701 #[test]
702 fn test_error_code_display_str() {
703 let code = ErrorCode::Str("auth_error".to_string());
704 assert_eq!(format!("{}", code), "auth_error");
705 }
706
707 #[test]
708 fn test_to_api_code_num() {
709 let code = ErrorCode::Num(401);
710 assert_eq!(to_api_code(&code), 401);
711 }
712
713 #[test]
714 fn test_to_api_code_str_valid() {
715 let code = ErrorCode::Str("429".to_string());
716 assert_eq!(to_api_code(&code), 429);
717 }
718
719 #[test]
720 fn test_to_api_code_str_invalid() {
721 let code = ErrorCode::Str("invalid".to_string());
722 assert_eq!(to_api_code(&code), 0);
723 }
724
725 #[test]
726 fn test_to_api_code_num_overflow() {
727 let code = ErrorCode::Num(99999);
728 assert_eq!(to_api_code(&code), 0);
729 }
730
731 #[test]
732 fn test_api_error_envelope_deserialize() {
733 let json = r#"{"error":{"code":401,"message":"Unauthorized"}}"#;
734 let envelope: ApiErrorEnvelope = serde_json::from_str(json).unwrap();
735 let (_code, message) = envelope.into_parts();
736 assert_eq!(message, "Unauthorized");
737 }
738
739 #[test]
740 fn test_api_error_envelope_deserialize_str_code() {
741 let json = r#"{"error":{"code":"1300","message":"Rate limit exceeded"}}"#;
742 let envelope: ApiErrorEnvelope = serde_json::from_str(json).unwrap();
743 let (code, message) = envelope.into_parts();
744 assert_eq!(message, "Rate limit exceeded");
745 assert_eq!(to_api_code(&code), 1300);
746 }
747
748 #[test]
749 fn test_api_error_envelope_deserialize_flat() {
750 let json = r#"{"code":1312,"message":"Quota exhausted"}"#;
751 let envelope: ApiErrorEnvelope = serde_json::from_str(json).unwrap();
752 let (code, message) = envelope.into_parts();
753 assert_eq!(message, "Quota exhausted");
754 assert_eq!(to_api_code(&code), 1312);
755 }
756
757 #[test]
758 fn test_parse_api_error_response_prefers_business_code() {
759 let error =
760 parse_api_error_response(429, r#"{"code":1312,"message":"Quota exhausted"}"#.into());
761 assert!(matches!(
762 error,
763 ZaiError::RateLimitError {
764 code: 1312,
765 message
766 } if message == "Quota exhausted"
767 ));
768 }
769
770 #[test]
771 fn test_parse_api_error_response_unparseable_body() {
772 let error = parse_api_error_response(500, "not json".to_string());
773 assert!(matches!(
774 error,
775 ZaiError::HttpError {
776 status: 500,
777 message
778 } if message == "Internal server error - try again later"
779 ));
780 }
781
782 #[test]
783 fn test_calculate_retry_delay_fixed() {
784 let delay = Duration::from_secs(2);
785 let strategy = RetryDelay::Fixed(delay);
786 assert_eq!(calculate_retry_delay(0, &strategy), delay);
787 assert_eq!(calculate_retry_delay(1, &strategy), delay);
788 assert_eq!(calculate_retry_delay(5, &strategy), delay);
789 }
790
791 #[test]
792 fn test_calculate_retry_delay_exponential() {
793 let base = Duration::from_millis(500);
794 let max = Duration::from_secs(5);
795 let strategy = RetryDelay::Exponential { base, max };
796
797 assert_eq!(
798 calculate_retry_delay(0, &strategy),
799 Duration::from_millis(500)
800 );
801 assert_eq!(
802 calculate_retry_delay(1, &strategy),
803 Duration::from_millis(1000)
804 );
805 assert_eq!(
806 calculate_retry_delay(2, &strategy),
807 Duration::from_millis(2000)
808 );
809 assert_eq!(
810 calculate_retry_delay(3, &strategy),
811 Duration::from_millis(4000)
812 );
813 assert_eq!(calculate_retry_delay(4, &strategy), max);
814 assert_eq!(calculate_retry_delay(10, &strategy), max);
815 }
816
817 #[test]
818 fn test_calculate_retry_delay_none() {
819 let strategy = RetryDelay::None;
820 assert_eq!(calculate_retry_delay(0, &strategy), Duration::ZERO);
821 assert_eq!(calculate_retry_delay(5, &strategy), Duration::ZERO);
822 }
823
824 #[test]
825 fn test_add_jitter() {
826 let delay = Duration::from_millis(1000);
827 let with_jitter = add_jitter(delay);
828
829 assert!(with_jitter >= delay);
831 assert!(with_jitter <= delay + Duration::from_millis(250));
832 }
833
834 #[test]
835 fn test_should_retry_server_error() {
836 let error = ZaiError::HttpError {
837 status: 500,
838 message: "Internal server error".to_string(),
839 };
840 assert!(should_retry(&error, 0, 3));
841 assert!(should_retry(&error, 2, 3));
842 assert!(!should_retry(&error, 3, 3));
843 }
844
845 #[test]
846 fn test_should_retry_gateway_timeout() {
847 let error = ZaiError::HttpError {
848 status: 504,
849 message: "Gateway timeout".to_string(),
850 };
851 assert!(should_retry(&error, 0, 3));
852 }
853
854 #[test]
855 fn test_should_retry_rate_limit() {
856 let error = ZaiError::RateLimitError {
857 code: 1301,
858 message: "Rate limit exceeded".to_string(),
859 };
860 assert!(should_retry(&error, 0, 3));
861 }
862
863 #[test]
864 fn test_should_retry_http_429() {
865 let error = ZaiError::HttpError {
866 status: 429,
867 message: "Too many requests".to_string(),
868 };
869 assert!(should_retry(&error, 0, 3));
870 }
871
872 #[test]
873 fn test_should_retry_network_error() {
874 let error = ZaiError::HttpError {
877 status: 503,
878 message: "Network error".to_string(),
879 };
880 assert!(should_retry(&error, 0, 3));
881 }
882
883 #[test]
884 fn test_should_not_retry_client_error() {
885 let error = ZaiError::HttpError {
886 status: 400,
887 message: "Bad request".to_string(),
888 };
889 assert!(!should_retry(&error, 0, 3));
890 }
891
892 #[test]
893 fn test_should_not_retry_unauthorized() {
894 let error = ZaiError::AuthError {
895 code: 1001,
896 message: "Invalid API key".to_string(),
897 };
898 assert!(!should_retry(&error, 0, 3));
899 }
900
901 #[test]
902 fn test_should_not_retry_account_error() {
903 let error = ZaiError::AccountError {
904 code: 1110,
905 message: "Account not found".to_string(),
906 };
907 assert!(!should_retry(&error, 0, 3));
908 }
909
910 #[test]
911 fn test_should_not_retry_not_found() {
912 let error = ZaiError::HttpError {
913 status: 404,
914 message: "Resource not found".to_string(),
915 };
916 assert!(!should_retry(&error, 0, 3));
917 }
918
919 #[test]
920 fn test_should_not_retry_sdk_timeout() {
921 let error = ZaiError::ApiError {
925 code: crate::client::error::codes::SDK_TIMEOUT,
926 message: "Timeout waiting for parsing result".to_string(),
927 };
928 assert!(!should_retry(&error, 0, 3));
929 }
930
931 #[test]
932 fn test_http_client_config_default() {
933 let config = HttpClientConfig::default();
934 assert_eq!(config.timeout, Duration::from_secs(60));
935 assert_eq!(config.max_retries, 3);
936 assert!(config.enable_compression);
937 matches!(config.retry_delay, RetryDelay::Exponential { .. });
938 }
939
940 #[test]
941 fn test_retry_delay_default() {
942 let delay = RetryDelay::default();
943 matches!(delay, RetryDelay::Exponential { base, max } if base == Duration::from_millis(500) && max == Duration::from_secs(5));
944 }
945}