Skip to main content

vectorizer_sdk/
http_transport.rs

1//! HTTP transport implementation using reqwest
2
3use async_trait::async_trait;
4use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
5use reqwest::{Client, ClientBuilder};
6use serde_json::Value;
7
8use crate::error::{Result, VectorizerError};
9use crate::transport::{Protocol, Transport};
10
11/// HTTP transport client
12pub struct HttpTransport {
13    client: Client,
14    base_url: String,
15}
16
17impl HttpTransport {
18    /// Create a new HTTP transport.
19    ///
20    /// The `api_key` argument carries either a raw Vectorizer API key
21    /// (created via `POST /auth/keys`) or a JWT minted by `POST /auth/login`.
22    /// The transport sniffs the shape — three dot-separated base64url
23    /// segments → JWT, sent as `Authorization: Bearer <token>`; otherwise
24    /// sent as `X-API-Key: <key>`. The server's auth middleware treats
25    /// Bearer-wrapped strings as JWTs and never falls back to the API-key
26    /// validator, so sending a raw API key under `Authorization: Bearer`
27    /// silently 401s. This sniff keeps the public method signature
28    /// unchanged while routing each credential down the path the server
29    /// actually accepts.
30    pub fn new(base_url: &str, api_key: Option<&str>, timeout_secs: u64) -> Result<Self> {
31        let mut headers = HeaderMap::new();
32        headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
33
34        if let Some(key) = api_key {
35            let (header_name, header_value) = if looks_like_jwt(key) {
36                ("Authorization", format!("Bearer {key}"))
37            } else {
38                ("X-API-Key", key.to_string())
39            };
40            headers.insert(
41                header_name,
42                HeaderValue::from_str(&header_value).map_err(|e| {
43                    VectorizerError::configuration(format!("Invalid auth credential: {e}"))
44                })?,
45            );
46        }
47
48        let client = ClientBuilder::new()
49            .timeout(std::time::Duration::from_secs(timeout_secs))
50            .default_headers(headers)
51            .build()
52            .map_err(|e| {
53                VectorizerError::configuration(format!("Failed to create HTTP client: {e}"))
54            })?;
55
56        Ok(Self {
57            client,
58            base_url: base_url.to_string(),
59        })
60    }
61}
62
63/// Cheap JWT shape sniff. A JWT is three base64url-encoded segments
64/// separated by `.`; every segment must be non-empty. Raw API keys
65/// generated by `POST /auth/keys` are a single 32-char alphanumeric
66/// string, so they fail this check and get routed to `X-API-Key`.
67fn looks_like_jwt(token: &str) -> bool {
68    let mut parts = token.split('.');
69    let Some(header) = parts.next() else {
70        return false;
71    };
72    let Some(payload) = parts.next() else {
73        return false;
74    };
75    let Some(signature) = parts.next() else {
76        return false;
77    };
78    if parts.next().is_some() {
79        return false;
80    }
81    !header.is_empty() && !payload.is_empty() && !signature.is_empty()
82}
83
84impl HttpTransport {
85    /// Make a generic request
86    async fn request(&self, method: &str, path: &str, body: Option<&Value>) -> Result<String> {
87        let url = format!("{}{}", self.base_url, path);
88
89        let mut request = match method {
90            "GET" => self.client.get(&url),
91            "POST" => self.client.post(&url),
92            "PUT" => self.client.put(&url),
93            "DELETE" => self.client.delete(&url),
94            _ => {
95                return Err(VectorizerError::configuration(format!(
96                    "Unsupported HTTP method: {method}"
97                )));
98            }
99        };
100
101        if let Some(data) = body {
102            request = request.json(data);
103        }
104
105        let response = request
106            .send()
107            .await
108            .map_err(|e| VectorizerError::network(format!("HTTP request failed: {e}")))?;
109
110        if !response.status().is_success() {
111            let status = response.status();
112            let error_text = response
113                .text()
114                .await
115                .unwrap_or_else(|_| "Unknown error".to_string());
116            return Err(VectorizerError::server(format!(
117                "HTTP {status}: {error_text}"
118            )));
119        }
120
121        response
122            .text()
123            .await
124            .map_err(|e| VectorizerError::network(format!("Failed to read response: {e}")))
125    }
126}
127
128#[async_trait]
129impl Transport for HttpTransport {
130    async fn get(&self, path: &str) -> Result<String> {
131        self.request("GET", path, None).await
132    }
133
134    async fn post(&self, path: &str, data: Option<&Value>) -> Result<String> {
135        self.request("POST", path, data).await
136    }
137
138    async fn put(&self, path: &str, data: Option<&Value>) -> Result<String> {
139        self.request("PUT", path, data).await
140    }
141
142    async fn delete(&self, path: &str) -> Result<String> {
143        self.request("DELETE", path, None).await
144    }
145
146    fn protocol(&self) -> Protocol {
147        Protocol::Http
148    }
149}
150
151impl HttpTransport {
152    /// Upload a file using multipart/form-data (not part of Transport trait)
153    pub async fn post_multipart(
154        &self,
155        path: &str,
156        file_bytes: Vec<u8>,
157        filename: &str,
158        form_fields: std::collections::HashMap<String, String>,
159    ) -> Result<String> {
160        let url = format!("{}{}", self.base_url, path);
161
162        // Create multipart form
163        let mut form = reqwest::multipart::Form::new();
164
165        // Add file
166        let file_part = reqwest::multipart::Part::bytes(file_bytes).file_name(filename.to_string());
167        form = form.part("file", file_part);
168
169        // Add other form fields
170        for (key, value) in form_fields {
171            form = form.text(key, value);
172        }
173
174        let response = self
175            .client
176            .post(&url)
177            .multipart(form)
178            .send()
179            .await
180            .map_err(|e| VectorizerError::network(format!("File upload failed: {e}")))?;
181
182        if !response.status().is_success() {
183            let status = response.status();
184            let error_text = response
185                .text()
186                .await
187                .unwrap_or_else(|_| "Unknown error".to_string());
188            return Err(VectorizerError::server(format!(
189                "HTTP {status}: {error_text}"
190            )));
191        }
192
193        response
194            .text()
195            .await
196            .map_err(|e| VectorizerError::network(format!("Failed to read response: {e}")))
197    }
198}