clob_client_rust/
http_helpers.rs

1use crate::errors::ClobError;
2use reqwest::Client;
3use serde::Serialize;
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6use std::collections::HashMap;
7
8pub const GET: &str = "GET";
9pub const POST: &str = "POST";
10pub const DELETE: &str = "DELETE";
11pub const PUT: &str = "PUT";
12
13pub type QueryParams = HashMap<String, String>;
14
15pub struct RequestOptions<B = Value> {
16    pub headers: Option<HashMap<String, String>>,
17    pub data: Option<B>,
18    pub params: Option<QueryParams>,
19}
20
21pub async fn post_typed<R, B>(
22    endpoint: &str,
23    options: Option<RequestOptions<B>>,
24) -> Result<R, ClobError>
25where
26    R: DeserializeOwned,
27    B: Serialize,
28{
29    let client = Client::new();
30    let mut req = client.post(endpoint);
31    let mut debug_headers: Option<std::collections::HashMap<String, String>> = None;
32    let mut debug_body: Option<String> = None;
33    let mut debug_params: Option<QueryParams> = None;
34    if let Some(opts) = options {
35        if let Some(h) = opts.headers {
36            // Capture headers for debug (with masking) before moving
37            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
38                let mut masked = std::collections::HashMap::new();
39                for (k, v) in h.iter() {
40                    let key = k.to_string();
41                    let val = if key.contains("PASSPHRASE") || key.contains("API_KEY") {
42                        if v.len() > 6 {
43                            format!("{}***", &v[..6])
44                        } else {
45                            "***".to_string()
46                        }
47                    } else if key.contains("SIGNATURE") {
48                        if v.len() > 12 {
49                            format!("{}...", &v[..12])
50                        } else {
51                            "***".to_string()
52                        }
53                    } else {
54                        v.to_string()
55                    };
56                    masked.insert(key, val);
57                }
58                debug_headers = Some(masked);
59            }
60            for (k, v) in h.iter() {
61                req = req.header(k, v);
62            }
63        }
64        if let Some(body) = opts.data {
65            // Serialize once for debug printing; req.json will produce the same representation
66            if (std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok())
67                && let Ok(b) = serde_json::to_string(&body) {
68                    debug_body = Some(b);
69                }
70            req = req.json(&body);
71        }
72        if let Some(params) = opts.params {
73            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
74                debug_params = Some(params.clone());
75            }
76            req = req.query(&params);
77        }
78    }
79    if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
80        eprintln!("[HTTP DEBUG] POST {}", endpoint);
81        if let Some(h) = &debug_headers {
82            eprintln!("  headers={:?}", h);
83        }
84        if let Some(p) = &debug_params {
85            eprintln!("  params={:?}", p);
86        }
87        if let Some(b) = &debug_body {
88            let preview = if b.len() > 800 {
89                format!("{}... ({} bytes)", &b[..800], b.len())
90            } else {
91                b.clone()
92            };
93            eprintln!("  body={}", preview);
94        }
95    }
96    let resp = req
97        .send()
98        .await
99        .map_err(|e| ClobError::Other(format!("HTTP request failed: {}", e)))?;
100
101    let status = resp.status();
102
103    // Check status code first, before trying to parse
104    if !status.is_success() {
105        // Get response body as text for error details
106        let body_text = resp
107            .text()
108            .await
109            .unwrap_or_else(|_| "<unable to read response body>".to_string());
110
111        eprintln!("❌ HTTP Error Response:");
112        eprintln!("   Status: {}", status);
113        eprintln!("   Endpoint: {}", endpoint);
114        eprintln!("   Response Body: {}", body_text);
115
116        return Err(ClobError::Other(format!(
117            "HTTP {} error from {}: {}",
118            status, endpoint, body_text
119        )));
120    }
121
122    // Try to parse JSON, with detailed error message if it fails
123    let body_text = resp
124        .text()
125        .await
126        .map_err(|e| ClobError::Other(format!("Failed to read response body: {}", e)))?;
127
128    match serde_json::from_str::<R>(&body_text) {
129        Ok(val) => Ok(val),
130        Err(e) => {
131            eprintln!("❌ JSON Parse Error:");
132            eprintln!("   Endpoint: {}", endpoint);
133            eprintln!("   Error: {}", e);
134            eprintln!("   Response Body: {}", body_text);
135
136            Err(ClobError::Other(format!(
137                "Failed to parse JSON response from {}: {}. Response body: {}",
138                endpoint,
139                e,
140                if body_text.len() > 500 {
141                    format!(
142                        "{}... (truncated, {} bytes total)",
143                        &body_text[..500],
144                        body_text.len()
145                    )
146                } else {
147                    body_text
148                }
149            )))
150        }
151    }
152}
153
154pub async fn post(endpoint: &str, options: Option<RequestOptions>) -> Result<Value, ClobError> {
155    post_typed::<Value, Value>(endpoint, options).await
156}
157
158pub async fn get_typed<R, B>(
159    endpoint: &str,
160    options: Option<RequestOptions<B>>,
161) -> Result<R, ClobError>
162where
163    R: DeserializeOwned,
164    B: Serialize,
165{
166    let client = Client::new();
167    let mut req = client.get(endpoint);
168    let mut debug_headers: Option<std::collections::HashMap<String, String>> = None;
169    let mut debug_params: Option<QueryParams> = None;
170    if let Some(opts) = options {
171        if let Some(h) = opts.headers {
172            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
173                let mut masked = std::collections::HashMap::new();
174                for (k, v) in h.iter() {
175                    let key = k.to_string();
176                    let val = if key.contains("PASSPHRASE") || key.contains("API_KEY") {
177                        if v.len() > 6 {
178                            format!("{}***", &v[..6])
179                        } else {
180                            "***".to_string()
181                        }
182                    } else if key.contains("SIGNATURE") {
183                        if v.len() > 12 {
184                            format!("{}...", &v[..12])
185                        } else {
186                            "***".to_string()
187                        }
188                    } else {
189                        v.to_string()
190                    };
191                    masked.insert(key, val);
192                }
193                debug_headers = Some(masked);
194            }
195            for (k, v) in h.iter() {
196                req = req.header(k, v);
197            }
198        }
199        if let Some(params) = opts.params {
200            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
201                debug_params = Some(params.clone());
202            }
203            req = req.query(&params);
204        }
205    }
206    if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
207        eprintln!("[HTTP DEBUG] GET {}", endpoint);
208        if let Some(h) = &debug_headers {
209            eprintln!("  headers={:?}", h);
210        }
211        if let Some(p) = &debug_params {
212            eprintln!("  params={:?}", p);
213        }
214    }
215    let resp = req
216        .send()
217        .await
218        .map_err(|e| ClobError::Other(format!("HTTP request failed: {}", e)))?;
219
220    let status = resp.status();
221
222    // Check status code first, before trying to parse
223    if !status.is_success() {
224        // Get response body as text for error details
225        let body_text = resp
226            .text()
227            .await
228            .unwrap_or_else(|_| "<unable to read response body>".to_string());
229
230        eprintln!("❌ HTTP Error Response:");
231        eprintln!("   Status: {}", status);
232        eprintln!("   Endpoint: {}", endpoint);
233        eprintln!("   Response Body: {}", body_text);
234
235        return Err(ClobError::Other(format!(
236            "HTTP {} error from {}: {}",
237            status, endpoint, body_text
238        )));
239    }
240
241    // Try to parse JSON, with detailed error message if it fails
242    let body_text = resp
243        .text()
244        .await
245        .map_err(|e| ClobError::Other(format!("Failed to read response body: {}", e)))?;
246
247    match serde_json::from_str::<R>(&body_text) {
248        Ok(val) => Ok(val),
249        Err(e) => {
250            eprintln!("❌ JSON Parse Error:");
251            eprintln!("   Endpoint: {}", endpoint);
252            eprintln!("   Error: {}", e);
253            eprintln!("   Response Body: {}", body_text);
254
255            Err(ClobError::Other(format!(
256                "Failed to parse JSON response from {}: {}. Response body: {}",
257                endpoint,
258                e,
259                if body_text.len() > 500 {
260                    format!(
261                        "{}... (truncated, {} bytes total)",
262                        &body_text[..500],
263                        body_text.len()
264                    )
265                } else {
266                    body_text
267                }
268            )))
269        }
270    }
271}
272
273pub async fn get(endpoint: &str, options: Option<RequestOptions>) -> Result<Value, ClobError> {
274    get_typed::<Value, Value>(endpoint, options).await
275}
276
277pub async fn del_typed<R, B>(
278    endpoint: &str,
279    options: Option<RequestOptions<B>>,
280) -> Result<R, ClobError>
281where
282    R: DeserializeOwned,
283    B: Serialize,
284{
285    let client = Client::new();
286    let mut req = client.delete(endpoint);
287    let mut debug_headers: Option<std::collections::HashMap<String, String>> = None;
288    let mut debug_body: Option<String> = None;
289    let mut debug_params: Option<QueryParams> = None;
290    if let Some(opts) = options {
291        if let Some(h) = opts.headers {
292            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
293                let mut masked = std::collections::HashMap::new();
294                for (k, v) in h.iter() {
295                    let key = k.to_string();
296                    let val = if key.contains("PASSPHRASE") || key.contains("API_KEY") {
297                        if v.len() > 6 {
298                            format!("{}***", &v[..6])
299                        } else {
300                            "***".to_string()
301                        }
302                    } else if key.contains("SIGNATURE") {
303                        if v.len() > 12 {
304                            format!("{}...", &v[..12])
305                        } else {
306                            "***".to_string()
307                        }
308                    } else {
309                        v.to_string()
310                    };
311                    masked.insert(key, val);
312                }
313                debug_headers = Some(masked);
314            }
315            for (k, v) in h.iter() {
316                req = req.header(k, v);
317            }
318        }
319        if let Some(body) = opts.data {
320            if (std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok())
321                && let Ok(b) = serde_json::to_string(&body) {
322                    debug_body = Some(b);
323                }
324            req = req.json(&body);
325        }
326        if let Some(params) = opts.params {
327            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
328                debug_params = Some(params.clone());
329            }
330            req = req.query(&params);
331        }
332    }
333    if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
334        eprintln!("[HTTP DEBUG] DELETE {}", endpoint);
335        if let Some(h) = &debug_headers {
336            eprintln!("  headers={:?}", h);
337        }
338        if let Some(p) = &debug_params {
339            eprintln!("  params={:?}", p);
340        }
341        if let Some(b) = &debug_body {
342            let preview = if b.len() > 800 {
343                format!("{}... ({} bytes)", &b[..800], b.len())
344            } else {
345                b.clone()
346            };
347            eprintln!("  body={}", preview);
348        }
349    }
350    let resp = req
351        .send()
352        .await
353        .map_err(|e| ClobError::Other(format!("HTTP request failed: {}", e)))?;
354
355    let status = resp.status();
356
357    // Check status code first, before trying to parse
358    if !status.is_success() {
359        // Get response body as text for error details
360        let body_text = resp
361            .text()
362            .await
363            .unwrap_or_else(|_| "<unable to read response body>".to_string());
364
365        eprintln!("❌ HTTP Error Response:");
366        eprintln!("   Status: {}", status);
367        eprintln!("   Endpoint: {}", endpoint);
368        eprintln!("   Response Body: {}", body_text);
369
370        return Err(ClobError::Other(format!(
371            "HTTP {} error from {}: {}",
372            status, endpoint, body_text
373        )));
374    }
375
376    // Try to parse JSON, with detailed error message if it fails
377    let body_text = resp
378        .text()
379        .await
380        .map_err(|e| ClobError::Other(format!("Failed to read response body: {}", e)))?;
381
382    match serde_json::from_str::<R>(&body_text) {
383        Ok(val) => Ok(val),
384        Err(e) => {
385            eprintln!("❌ JSON Parse Error:");
386            eprintln!("   Endpoint: {}", endpoint);
387            eprintln!("   Error: {}", e);
388            eprintln!("   Response Body: {}", body_text);
389
390            Err(ClobError::Other(format!(
391                "Failed to parse JSON response from {}: {}. Response body: {}",
392                endpoint,
393                e,
394                if body_text.len() > 500 {
395                    format!(
396                        "{}... (truncated, {} bytes total)",
397                        &body_text[..500],
398                        body_text.len()
399                    )
400                } else {
401                    body_text
402                }
403            )))
404        }
405    }
406}
407
408pub async fn del(endpoint: &str, options: Option<RequestOptions>) -> Result<Value, ClobError> {
409    del_typed::<Value, Value>(endpoint, options).await
410}
411
412pub fn parse_orders_scoring_params(order_ids: Option<&Vec<String>>) -> QueryParams {
413    let mut params = QueryParams::new();
414    if let Some(ids) = order_ids {
415        params.insert("order_ids".to_string(), ids.join(","));
416    }
417    params
418}
419
420pub fn parse_drop_notification_params(ids: Option<&Vec<String>>) -> QueryParams {
421    let mut params = QueryParams::new();
422    if let Some(arr) = ids {
423        params.insert("ids".to_string(), arr.join(","));
424    }
425    params
426}