1use std::time::Duration;
7
8use crate::telemetry;
9
10#[derive(Debug, Clone)]
12pub struct HttpConfig {
13 pub timeout: Duration,
15 pub max_retries: u32,
17 pub retry_base_delay: Duration,
19 pub retry_max_delay: Duration,
21 pub user_agent: String,
23}
24
25impl Default for HttpConfig {
26 fn default() -> Self {
27 Self {
28 timeout: Duration::from_secs(60),
29 max_retries: 3,
30 retry_base_delay: Duration::from_millis(500),
31 retry_max_delay: Duration::from_secs(30),
32 user_agent: format!("gemini-live/{}", env!("CARGO_PKG_VERSION")),
33 }
34 }
35}
36
37#[derive(Debug, thiserror::Error)]
39pub enum HttpError {
40 #[error("HTTP request failed: {0}")]
42 Request(#[from] reqwest::Error),
43
44 #[error("API error {status}: {message}")]
46 ApiError {
47 status: u16,
49 message: String,
51 body: Option<serde_json::Value>,
53 },
54
55 #[error("Auth error: {0}")]
57 Auth(String),
58
59 #[error("JSON parse error: {0}")]
61 Json(#[from] serde_json::Error),
62
63 #[error("All {attempts} retries exhausted: {last_error}")]
65 RetriesExhausted {
66 attempts: u32,
68 last_error: String,
70 },
71}
72
73pub struct HttpClient {
75 inner: reqwest::Client,
76 config: HttpConfig,
77}
78
79impl HttpClient {
80 pub fn new(config: HttpConfig) -> Self {
82 let inner = reqwest::Client::builder()
83 .timeout(config.timeout)
84 .user_agent(&config.user_agent)
85 .build()
86 .expect("Failed to build reqwest client");
87 Self { inner, config }
88 }
89
90 pub async fn post_json(
92 &self,
93 url: &str,
94 auth_headers: Vec<(String, String)>,
95 body: &impl serde::Serialize,
96 ) -> Result<serde_json::Value, HttpError> {
97 self.request_with_retry("POST", url, auth_headers, Some(body))
98 .await
99 }
100
101 pub async fn patch_json(
103 &self,
104 url: &str,
105 auth_headers: Vec<(String, String)>,
106 body: &impl serde::Serialize,
107 ) -> Result<serde_json::Value, HttpError> {
108 self.request_with_retry("PATCH", url, auth_headers, Some(body))
109 .await
110 }
111
112 pub async fn put_json(
114 &self,
115 url: &str,
116 auth_headers: Vec<(String, String)>,
117 body: &impl serde::Serialize,
118 ) -> Result<serde_json::Value, HttpError> {
119 self.request_with_retry("PUT", url, auth_headers, Some(body))
120 .await
121 }
122
123 pub async fn get_json(
125 &self,
126 url: &str,
127 auth_headers: Vec<(String, String)>,
128 ) -> Result<serde_json::Value, HttpError> {
129 self.request_with_retry::<()>("GET", url, auth_headers, None)
130 .await
131 }
132
133 pub async fn delete(
135 &self,
136 url: &str,
137 auth_headers: Vec<(String, String)>,
138 ) -> Result<serde_json::Value, HttpError> {
139 self.request_with_retry::<()>("DELETE", url, auth_headers, None)
140 .await
141 }
142
143 async fn request_with_retry<B: serde::Serialize>(
145 &self,
146 method: &str,
147 url: &str,
148 auth_headers: Vec<(String, String)>,
149 body: Option<&B>,
150 ) -> Result<serde_json::Value, HttpError> {
151 let mut last_error = String::new();
152
153 for attempt in 0..=self.config.max_retries {
154 if attempt > 0 {
155 let delay = self.backoff_delay(attempt);
156 telemetry::logging::log_http_retry(url, attempt, delay.as_millis() as u64);
157 tokio::time::sleep(delay).await;
158 }
159
160 telemetry::logging::log_http_request(method, url);
161 let _span = telemetry::spans::http_request_span(method, url);
162
163 let start = std::time::Instant::now();
164 match self.execute_request(method, url, &auth_headers, body).await {
165 Ok(response) => {
166 let status = response.status();
167 let duration_ms = start.elapsed().as_millis() as f64;
168 telemetry::metrics::record_http_request(method, status.as_u16(), duration_ms);
169 telemetry::logging::log_http_response(status.as_u16(), duration_ms);
170
171 if status.is_success() {
172 let body = response.text().await?;
173 if body.is_empty() {
174 return Ok(serde_json::Value::Null);
175 }
176 return Ok(serde_json::from_str(&body)?);
177 }
178
179 let status_code = status.as_u16();
180 let body_text = response.text().await.unwrap_or_default();
181 let body_json: Option<serde_json::Value> =
182 serde_json::from_str(&body_text).ok();
183
184 let message = body_json
186 .as_ref()
187 .and_then(|v| v.get("error"))
188 .and_then(|v| v.get("message"))
189 .and_then(|v| v.as_str())
190 .unwrap_or(&body_text)
191 .to_string();
192
193 if is_retryable_status(status_code) && attempt < self.config.max_retries {
195 last_error = format!("HTTP {status_code}: {message}");
196 continue;
197 }
198
199 return Err(HttpError::ApiError {
200 status: status_code,
201 message,
202 body: body_json,
203 });
204 }
205 Err(e) => {
206 let duration_ms = start.elapsed().as_millis() as f64;
207 telemetry::metrics::record_http_request(method, 0, duration_ms);
208
209 if is_retryable_error(&e) && attempt < self.config.max_retries {
210 last_error = e.to_string();
211 continue;
212 }
213 return Err(HttpError::Request(e));
214 }
215 }
216 }
217
218 Err(HttpError::RetriesExhausted {
219 attempts: self.config.max_retries + 1,
220 last_error,
221 })
222 }
223
224 async fn execute_request<B: serde::Serialize>(
226 &self,
227 method: &str,
228 url: &str,
229 auth_headers: &[(String, String)],
230 body: Option<&B>,
231 ) -> Result<reqwest::Response, reqwest::Error> {
232 let mut builder = match method {
233 "POST" => self.inner.post(url),
234 "GET" => self.inner.get(url),
235 "DELETE" => self.inner.delete(url),
236 "PATCH" => self.inner.patch(url),
237 "PUT" => self.inner.put(url),
238 _ => self
239 .inner
240 .request(reqwest::Method::from_bytes(method.as_bytes()).unwrap(), url),
241 };
242
243 for (key, value) in auth_headers {
244 builder = builder.header(key, value);
245 }
246
247 if let Some(body) = body {
248 builder = builder.json(body);
249 }
250
251 builder.send().await
252 }
253
254 fn backoff_delay(&self, attempt: u32) -> Duration {
256 let delay = self.config.retry_base_delay * 2u32.saturating_pow(attempt.saturating_sub(1));
257 std::cmp::min(delay, self.config.retry_max_delay)
258 }
259}
260
261fn is_retryable_status(status: u16) -> bool {
263 status == 429 || (500..600).contains(&status)
264}
265
266fn is_retryable_error(e: &reqwest::Error) -> bool {
268 e.is_timeout() || e.is_connect() || e.is_request()
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn default_config() {
277 let config = HttpConfig::default();
278 assert_eq!(config.timeout, Duration::from_secs(60));
279 assert_eq!(config.max_retries, 3);
280 assert!(config.user_agent.starts_with("gemini-live/"));
281 }
282
283 #[test]
284 fn backoff_delay_calculation() {
285 let client = HttpClient::new(HttpConfig {
286 retry_base_delay: Duration::from_millis(100),
287 retry_max_delay: Duration::from_secs(5),
288 ..HttpConfig::default()
289 });
290 assert_eq!(client.backoff_delay(1), Duration::from_millis(100));
291 assert_eq!(client.backoff_delay(2), Duration::from_millis(200));
292 assert_eq!(client.backoff_delay(3), Duration::from_millis(400));
293 }
294
295 #[test]
296 fn backoff_delay_capped() {
297 let client = HttpClient::new(HttpConfig {
298 retry_base_delay: Duration::from_secs(1),
299 retry_max_delay: Duration::from_secs(5),
300 ..HttpConfig::default()
301 });
302 assert_eq!(client.backoff_delay(10), Duration::from_secs(5));
304 }
305
306 #[test]
307 fn retryable_status_codes() {
308 assert!(is_retryable_status(429));
309 assert!(is_retryable_status(500));
310 assert!(is_retryable_status(503));
311 assert!(is_retryable_status(599));
312 assert!(!is_retryable_status(400));
313 assert!(!is_retryable_status(404));
314 assert!(!is_retryable_status(200));
315 }
316}