Skip to main content

logtail_rust/http_client/
mod.rs

1mod base_client;
2pub mod service;
3
4pub use base_client::ReqwestClient;
5
6use reqwest::header::HeaderMap;
7use serde_json::Value;
8use std::future::Future;
9use std::time::Duration;
10
11#[derive(Debug, thiserror::Error)]
12pub enum LogtailError {
13    #[error("HTTP {status}: {message}")]
14    Http { status: u16, message: String },
15    #[error("serialization failed: {0}")]
16    Serialization(#[from] serde_json::Error),
17    #[error("network error: {0}")]
18    Network(#[from] reqwest::Error),
19}
20
21impl LogtailError {
22    pub fn is_retryable(&self) -> bool {
23        match self {
24            LogtailError::Http { status, .. } => *status >= 500,
25            LogtailError::Network(_) => true,
26            LogtailError::Serialization(_) => false,
27        }
28    }
29}
30
31pub struct RetryConfig {
32    pub max_retries: u32,
33    pub base_delay: Duration,
34    pub max_delay: Duration,
35    pub jitter: bool,
36}
37
38impl Default for RetryConfig {
39    fn default() -> Self {
40        Self {
41            max_retries: 3,
42            base_delay: Duration::from_secs(1),
43            max_delay: Duration::from_secs(5),
44            jitter: true,
45        }
46    }
47}
48
49pub trait HttpClient: Send + Sync {
50    fn post_json(
51        &self,
52        url: &str,
53        body: &Value,
54        extra_headers: Option<HeaderMap>,
55    ) -> impl Future<Output = Result<Option<Value>, LogtailError>> + Send;
56}
57
58#[cfg(test)]
59pub(crate) mod mock;
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    // --- is_retryable ---
66
67    #[test]
68    fn http_5xx_is_retryable() {
69        let err = LogtailError::Http {
70            status: 500,
71            message: "internal".to_string(),
72        };
73        assert!(err.is_retryable());
74    }
75
76    #[test]
77    fn http_502_is_retryable() {
78        let err = LogtailError::Http {
79            status: 502,
80            message: "bad gateway".to_string(),
81        };
82        assert!(err.is_retryable());
83    }
84
85    #[test]
86    fn http_4xx_is_not_retryable() {
87        let err = LogtailError::Http {
88            status: 400,
89            message: "bad request".to_string(),
90        };
91        assert!(!err.is_retryable());
92    }
93
94    #[test]
95    fn http_404_is_not_retryable() {
96        let err = LogtailError::Http {
97            status: 404,
98            message: "not found".to_string(),
99        };
100        assert!(!err.is_retryable());
101    }
102
103    #[test]
104    fn serialization_is_not_retryable() {
105        let serde_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
106        let err = LogtailError::Serialization(serde_err);
107        assert!(!err.is_retryable());
108    }
109
110    // --- RetryConfig::default ---
111
112    #[test]
113    fn retry_config_default_values() {
114        let config = RetryConfig::default();
115        assert_eq!(config.max_retries, 3);
116        assert_eq!(config.base_delay, Duration::from_secs(1));
117        assert_eq!(config.max_delay, Duration::from_secs(5));
118        assert!(config.jitter);
119    }
120}