agentic_connect/engine/
http_client.rs1use std::collections::HashMap;
4use std::time::Instant;
5
6use crate::types::{ConnectError, ConnectResult};
7
8#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct HttpResponse {
11 pub status: u16,
12 pub headers: HashMap<String, String>,
13 pub body: String,
14 pub latency_ms: u64,
15 pub url: String,
16 pub method: String,
17}
18
19#[cfg(feature = "http")]
21pub async fn http_request(
22 url: &str,
23 method: &str,
24 headers: Option<&HashMap<String, String>>,
25 body: Option<&str>,
26 timeout_ms: u64,
27) -> ConnectResult<HttpResponse> {
28 let client = reqwest::Client::builder()
29 .timeout(std::time::Duration::from_millis(timeout_ms))
30 .build()
31 .map_err(|e| ConnectError::Http(e.to_string()))?;
32
33 let mut req = match method.to_uppercase().as_str() {
34 "GET" => client.get(url),
35 "POST" => client.post(url),
36 "PUT" => client.put(url),
37 "PATCH" => client.patch(url),
38 "DELETE" => client.delete(url),
39 "HEAD" => client.head(url),
40 other => return Err(ConnectError::NotSupported(format!("HTTP method: {}", other))),
41 };
42
43 if let Some(hdrs) = headers {
44 for (k, v) in hdrs {
45 req = req.header(k.as_str(), v.as_str());
46 }
47 }
48
49 if let Some(b) = body {
50 req = req.body(b.to_string());
51 if headers.map_or(true, |h| !h.contains_key("content-type")) {
53 req = req.header("content-type", "application/json");
54 }
55 }
56
57 let start = Instant::now();
58 let resp = req.send().await?;
59 let latency_ms = start.elapsed().as_millis() as u64;
60
61 let status = resp.status().as_u16();
62 let resp_headers: HashMap<String, String> = resp
63 .headers()
64 .iter()
65 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
66 .collect();
67 let resp_body = resp.text().await.unwrap_or_default();
68
69 Ok(HttpResponse {
70 status,
71 headers: resp_headers,
72 body: resp_body,
73 latency_ms,
74 url: url.to_string(),
75 method: method.to_uppercase(),
76 })
77}
78
79pub fn extract_rate_limit(headers: &HashMap<String, String>) -> Option<(u32, u32, i64)> {
81 let limit = headers.get("x-ratelimit-limit")
82 .or_else(|| headers.get("ratelimit-limit"))
83 .and_then(|v| v.parse::<u32>().ok())?;
84 let remaining = headers.get("x-ratelimit-remaining")
85 .or_else(|| headers.get("ratelimit-remaining"))
86 .and_then(|v| v.parse::<u32>().ok())?;
87 let reset = headers.get("x-ratelimit-reset")
88 .or_else(|| headers.get("ratelimit-reset"))
89 .and_then(|v| v.parse::<i64>().ok())
90 .unwrap_or(0);
91 Some((limit, remaining, reset))
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn test_extract_rate_limit() {
100 let mut headers = HashMap::new();
101 headers.insert("x-ratelimit-limit".into(), "100".into());
102 headers.insert("x-ratelimit-remaining".into(), "42".into());
103 headers.insert("x-ratelimit-reset".into(), "1710000000".into());
104 let (limit, remaining, reset) = extract_rate_limit(&headers).unwrap();
105 assert_eq!(limit, 100);
106 assert_eq!(remaining, 42);
107 assert_eq!(reset, 1710000000);
108 }
109
110 #[test]
111 fn test_extract_rate_limit_missing() {
112 let headers = HashMap::new();
113 assert!(extract_rate_limit(&headers).is_none());
114 }
115}