vectorizer_sdk/
http_transport.rs1use std::time::Duration;
4
5use async_trait::async_trait;
6use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
7use reqwest::{Client, ClientBuilder};
8use serde_json::Value;
9
10use crate::error::{Result, VectorizerError};
11use crate::transport::{Protocol, Transport};
12
13const RETRY_AFTER_MAX_ATTEMPTS: u32 = 3;
16const RETRY_AFTER_MAX_SECS: u64 = 30;
20const RETRY_AFTER_DEFAULT_SECS: u64 = 1;
23
24pub struct HttpTransport {
26 client: Client,
27 base_url: String,
28}
29
30impl HttpTransport {
31 pub fn new(base_url: &str, api_key: Option<&str>, timeout_secs: u64) -> Result<Self> {
44 let mut headers = HeaderMap::new();
45 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
46
47 if let Some(key) = api_key {
48 let (header_name, header_value) = if looks_like_jwt(key) {
49 ("Authorization", format!("Bearer {key}"))
50 } else {
51 ("X-API-Key", key.to_string())
52 };
53 headers.insert(
54 header_name,
55 HeaderValue::from_str(&header_value).map_err(|e| {
56 VectorizerError::configuration(format!("Invalid auth credential: {e}"))
57 })?,
58 );
59 }
60
61 let client = ClientBuilder::new()
62 .timeout(std::time::Duration::from_secs(timeout_secs))
63 .default_headers(headers)
64 .build()
65 .map_err(|e| {
66 VectorizerError::configuration(format!("Failed to create HTTP client: {e}"))
67 })?;
68
69 Ok(Self {
70 client,
71 base_url: base_url.to_string(),
72 })
73 }
74}
75
76fn looks_like_jwt(token: &str) -> bool {
81 let mut parts = token.split('.');
82 let Some(header) = parts.next() else {
83 return false;
84 };
85 let Some(payload) = parts.next() else {
86 return false;
87 };
88 let Some(signature) = parts.next() else {
89 return false;
90 };
91 if parts.next().is_some() {
92 return false;
93 }
94 !header.is_empty() && !payload.is_empty() && !signature.is_empty()
95}
96
97impl HttpTransport {
98 async fn request(&self, method: &str, path: &str, body: Option<&Value>) -> Result<String> {
103 let url = format!("{}{}", self.base_url, path);
104 let mut attempts_remaining = RETRY_AFTER_MAX_ATTEMPTS;
105
106 loop {
107 let mut request = match method {
108 "GET" => self.client.get(&url),
109 "POST" => self.client.post(&url),
110 "PUT" => self.client.put(&url),
111 "DELETE" => self.client.delete(&url),
112 _ => {
113 return Err(VectorizerError::configuration(format!(
114 "Unsupported HTTP method: {method}"
115 )));
116 }
117 };
118
119 if let Some(data) = body {
120 request = request.json(data);
121 }
122
123 let response = request
124 .send()
125 .await
126 .map_err(|e| VectorizerError::network(format!("HTTP request failed: {e}")))?;
127
128 if response.status().as_u16() == 429 {
129 let retry_after = parse_retry_after_secs(
130 response
131 .headers()
132 .get(reqwest::header::RETRY_AFTER)
133 .and_then(|v| v.to_str().ok()),
134 );
135 let body_text = response
136 .text()
137 .await
138 .unwrap_or_else(|_| "Unknown error".to_string());
139
140 if attempts_remaining == 0 {
141 return Err(VectorizerError::rate_limit(format!(
142 "HTTP 429 after {RETRY_AFTER_MAX_ATTEMPTS} retries: {body_text}",
143 )));
144 }
145
146 tracing::info!(
147 "Vectorizer 429 — sleeping {retry_after:?} before retry \
148 (remaining attempts={attempts_remaining})",
149 );
150 attempts_remaining -= 1;
151 tokio::time::sleep(retry_after).await;
152 continue;
153 }
154
155 if !response.status().is_success() {
156 let status = response.status();
157 let error_text = response
158 .text()
159 .await
160 .unwrap_or_else(|_| "Unknown error".to_string());
161 return Err(VectorizerError::server(format!(
162 "HTTP {status}: {error_text}"
163 )));
164 }
165
166 return response
167 .text()
168 .await
169 .map_err(|e| VectorizerError::network(format!("Failed to read response: {e}")));
170 }
171 }
172}
173
174#[doc(hidden)]
180pub fn parse_retry_after_secs(value: Option<&str>) -> Duration {
181 let raw = match value {
182 Some(v) => v.trim(),
183 None => return Duration::from_secs(RETRY_AFTER_DEFAULT_SECS),
184 };
185 let secs = raw.parse::<u64>().unwrap_or(RETRY_AFTER_DEFAULT_SECS);
186 let secs = if secs == 0 {
187 RETRY_AFTER_DEFAULT_SECS
188 } else {
189 secs.min(RETRY_AFTER_MAX_SECS)
190 };
191 Duration::from_secs(secs)
192}
193
194#[async_trait]
195impl Transport for HttpTransport {
196 async fn get(&self, path: &str) -> Result<String> {
197 self.request("GET", path, None).await
198 }
199
200 async fn post(&self, path: &str, data: Option<&Value>) -> Result<String> {
201 self.request("POST", path, data).await
202 }
203
204 async fn put(&self, path: &str, data: Option<&Value>) -> Result<String> {
205 self.request("PUT", path, data).await
206 }
207
208 async fn delete(&self, path: &str) -> Result<String> {
209 self.request("DELETE", path, None).await
210 }
211
212 fn protocol(&self) -> Protocol {
213 Protocol::Http
214 }
215}
216
217impl HttpTransport {
218 pub async fn post_multipart(
220 &self,
221 path: &str,
222 file_bytes: Vec<u8>,
223 filename: &str,
224 form_fields: std::collections::HashMap<String, String>,
225 ) -> Result<String> {
226 let url = format!("{}{}", self.base_url, path);
227
228 let mut form = reqwest::multipart::Form::new();
230
231 let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(filename.to_string());
233 form = form.part("file", file_part);
234
235 for (key, value) in form_fields {
237 form = form.text(key, value);
238 }
239
240 let response = self
241 .client
242 .post(&url)
243 .multipart(form)
244 .send()
245 .await
246 .map_err(|e| VectorizerError::network(format!("File upload failed: {e}")))?;
247
248 if !response.status().is_success() {
249 let status = response.status();
250 let error_text = response
251 .text()
252 .await
253 .unwrap_or_else(|_| "Unknown error".to_string());
254 return Err(VectorizerError::server(format!(
255 "HTTP {status}: {error_text}"
256 )));
257 }
258
259 response
260 .text()
261 .await
262 .map_err(|e| VectorizerError::network(format!("Failed to read response: {e}")))
263 }
264}