clob_client_rust/
http_helpers.rs

1use crate::errors::ClobError;
2use reqwest::Client;
3use serde::de::DeserializeOwned;
4use serde::Serialize;
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                if let Ok(b) = serde_json::to_string(&body) {
68                    debug_body = Some(b);
69                }
70            }
71            req = req.json(&body);
72        }
73        if let Some(params) = opts.params {
74            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
75                debug_params = Some(params.clone());
76            }
77            req = req.query(&params);
78        }
79    }
80    if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
81        eprintln!("[HTTP DEBUG] POST {}", endpoint);
82        if let Some(h) = &debug_headers {
83            eprintln!("  headers={:?}", h);
84        }
85        if let Some(p) = &debug_params {
86            eprintln!("  params={:?}", p);
87        }
88        if let Some(b) = &debug_body {
89            let preview = if b.len() > 800 {
90                format!("{}... ({} bytes)", &b[..800], b.len())
91            } else {
92                b.clone()
93            };
94            eprintln!("  body={}", preview);
95        }
96    }
97    let resp = req
98        .send()
99        .await
100        .map_err(|e| ClobError::Other(format!("HTTP request failed: {}", e)))?;
101
102    let status = resp.status();
103
104    // Check status code first, before trying to parse
105    if !status.is_success() {
106        // Get response body as text for error details
107        let body_text = resp
108            .text()
109            .await
110            .unwrap_or_else(|_| "<unable to read response body>".to_string());
111
112        eprintln!("❌ HTTP Error Response:");
113        eprintln!("   Status: {}", status);
114        eprintln!("   Endpoint: {}", endpoint);
115        eprintln!("   Response Body: {}", body_text);
116
117        return Err(ClobError::Other(format!(
118            "HTTP {} error from {}: {}",
119            status, endpoint, body_text
120        )));
121    }
122
123    // Try to parse JSON, with detailed error message if it fails
124    let body_text = resp
125        .text()
126        .await
127        .map_err(|e| ClobError::Other(format!("Failed to read response body: {}", e)))?;
128
129    match serde_json::from_str::<R>(&body_text) {
130        Ok(val) => Ok(val),
131        Err(e) => {
132            eprintln!("❌ JSON Parse Error:");
133            eprintln!("   Endpoint: {}", endpoint);
134            eprintln!("   Error: {}", e);
135            eprintln!("   Response Body: {}", body_text);
136
137            Err(ClobError::Other(format!(
138                "Failed to parse JSON response from {}: {}. Response body: {}",
139                endpoint,
140                e,
141                if body_text.len() > 500 {
142                    format!(
143                        "{}... (truncated, {} bytes total)",
144                        &body_text[..500],
145                        body_text.len()
146                    )
147                } else {
148                    body_text
149                }
150            )))
151        }
152    }
153}
154
155pub async fn post(endpoint: &str, options: Option<RequestOptions>) -> Result<Value, ClobError> {
156    post_typed::<Value, Value>(endpoint, options).await
157}
158
159pub async fn put_typed<R, B>(
160    endpoint: &str,
161    options: Option<RequestOptions<B>>,
162) -> Result<R, ClobError>
163where
164    R: DeserializeOwned,
165    B: Serialize,
166{
167    let client = Client::new();
168    let mut req = client.put(endpoint);
169    let mut debug_headers: Option<std::collections::HashMap<String, String>> = None;
170    let mut debug_body: Option<String> = None;
171    let mut debug_params: Option<QueryParams> = None;
172    if let Some(opts) = options {
173        if let Some(h) = opts.headers {
174            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
175                let mut masked = std::collections::HashMap::new();
176                for (k, v) in h.iter() {
177                    let key = k.to_string();
178                    let val = if key.contains("PASSPHRASE") || key.contains("API_KEY") {
179                        if v.len() > 6 {
180                            format!("{}***", &v[..6])
181                        } else {
182                            "***".to_string()
183                        }
184                    } else if key.contains("SIGNATURE") {
185                        if v.len() > 12 {
186                            format!("{}...", &v[..12])
187                        } else {
188                            "***".to_string()
189                        }
190                    } else {
191                        v.to_string()
192                    };
193                    masked.insert(key, val);
194                }
195                debug_headers = Some(masked);
196            }
197            for (k, v) in h.iter() {
198                req = req.header(k, v);
199            }
200        }
201        if let Some(body) = opts.data {
202            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
203                if let Ok(b) = serde_json::to_string(&body) {
204                    debug_body = Some(b);
205                }
206            }
207            req = req.json(&body);
208        }
209        if let Some(params) = opts.params {
210            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
211                debug_params = Some(params.clone());
212            }
213            req = req.query(&params);
214        }
215    }
216    if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
217        eprintln!("[HTTP DEBUG] PUT {}", endpoint);
218        if let Some(h) = &debug_headers {
219            eprintln!("  headers={:?}", h);
220        }
221        if let Some(p) = &debug_params {
222            eprintln!("  params={:?}", p);
223        }
224        if let Some(b) = &debug_body {
225            let preview = if b.len() > 800 {
226                format!("{}... ({} bytes)", &b[..800], b.len())
227            } else {
228                b.clone()
229            };
230            eprintln!("  body={}", preview);
231        }
232    }
233    let resp = req
234        .send()
235        .await
236        .map_err(|e| ClobError::Other(format!("HTTP request failed: {}", e)))?;
237
238    let status = resp.status();
239    if !status.is_success() {
240        let body_text = resp
241            .text()
242            .await
243            .unwrap_or_else(|_| "<unable to read response body>".to_string());
244
245        eprintln!("❌ HTTP Error Response:");
246        eprintln!("   Status: {}", status);
247        eprintln!("   Endpoint: {}", endpoint);
248        eprintln!("   Response Body: {}", body_text);
249
250        return Err(ClobError::Other(format!(
251            "HTTP {} error from {}: {}",
252            status, endpoint, body_text
253        )));
254    }
255
256    let body_text = resp
257        .text()
258        .await
259        .map_err(|e| ClobError::Other(format!("Failed to read response body: {}", e)))?;
260
261    match serde_json::from_str::<R>(&body_text) {
262        Ok(val) => Ok(val),
263        Err(e) => {
264            eprintln!("❌ JSON Parse Error:");
265            eprintln!("   Endpoint: {}", endpoint);
266            eprintln!("   Error: {}", e);
267            eprintln!("   Response Body: {}", body_text);
268
269            Err(ClobError::Other(format!(
270                "Failed to parse JSON response from {}: {}. Response body: {}",
271                endpoint,
272                e,
273                if body_text.len() > 500 {
274                    format!(
275                        "{}... (truncated, {} bytes total)",
276                        &body_text[..500],
277                        body_text.len()
278                    )
279                } else {
280                    body_text
281                }
282            )))
283        }
284    }
285}
286
287pub async fn put(endpoint: &str, options: Option<RequestOptions>) -> Result<Value, ClobError> {
288    put_typed::<Value, Value>(endpoint, options).await
289}
290
291pub async fn get_typed<R, B>(
292    endpoint: &str,
293    options: Option<RequestOptions<B>>,
294) -> Result<R, ClobError>
295where
296    R: DeserializeOwned,
297    B: Serialize,
298{
299    let client = Client::new();
300    let mut req = client.get(endpoint);
301    let mut debug_headers: Option<std::collections::HashMap<String, String>> = None;
302    let mut debug_params: Option<QueryParams> = None;
303    if let Some(opts) = options {
304        if let Some(h) = opts.headers {
305            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
306                let mut masked = std::collections::HashMap::new();
307                for (k, v) in h.iter() {
308                    let key = k.to_string();
309                    let val = if key.contains("PASSPHRASE") || key.contains("API_KEY") {
310                        if v.len() > 6 {
311                            format!("{}***", &v[..6])
312                        } else {
313                            "***".to_string()
314                        }
315                    } else if key.contains("SIGNATURE") {
316                        if v.len() > 12 {
317                            format!("{}...", &v[..12])
318                        } else {
319                            "***".to_string()
320                        }
321                    } else {
322                        v.to_string()
323                    };
324                    masked.insert(key, val);
325                }
326                debug_headers = Some(masked);
327            }
328            for (k, v) in h.iter() {
329                req = req.header(k, v);
330            }
331        }
332        if let Some(params) = opts.params {
333            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
334                debug_params = Some(params.clone());
335            }
336            req = req.query(&params);
337        }
338    }
339    if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
340        eprintln!("[HTTP DEBUG] GET {}", endpoint);
341        if let Some(h) = &debug_headers {
342            eprintln!("  headers={:?}", h);
343        }
344        if let Some(p) = &debug_params {
345            eprintln!("  params={:?}", p);
346        }
347    }
348    let resp = req
349        .send()
350        .await
351        .map_err(|e| ClobError::Other(format!("HTTP request failed: {}", e)))?;
352
353    let status = resp.status();
354
355    // Check status code first, before trying to parse
356    if !status.is_success() {
357        // Get response body as text for error details
358        let body_text = resp
359            .text()
360            .await
361            .unwrap_or_else(|_| "<unable to read response body>".to_string());
362
363        eprintln!("❌ HTTP Error Response:");
364        eprintln!("   Status: {}", status);
365        eprintln!("   Endpoint: {}", endpoint);
366        eprintln!("   Response Body: {}", body_text);
367
368        return Err(ClobError::Other(format!(
369            "HTTP {} error from {}: {}",
370            status, endpoint, body_text
371        )));
372    }
373
374    // Try to parse JSON, with detailed error message if it fails
375    let body_text = resp
376        .text()
377        .await
378        .map_err(|e| ClobError::Other(format!("Failed to read response body: {}", e)))?;
379
380    match serde_json::from_str::<R>(&body_text) {
381        Ok(val) => Ok(val),
382        Err(e) => {
383            eprintln!("❌ JSON Parse Error:");
384            eprintln!("   Endpoint: {}", endpoint);
385            eprintln!("   Error: {}", e);
386            eprintln!("   Response Body: {}", body_text);
387
388            Err(ClobError::Other(format!(
389                "Failed to parse JSON response from {}: {}. Response body: {}",
390                endpoint,
391                e,
392                if body_text.len() > 500 {
393                    format!(
394                        "{}... (truncated, {} bytes total)",
395                        &body_text[..500],
396                        body_text.len()
397                    )
398                } else {
399                    body_text
400                }
401            )))
402        }
403    }
404}
405
406pub async fn get(endpoint: &str, options: Option<RequestOptions>) -> Result<Value, ClobError> {
407    get_typed::<Value, Value>(endpoint, options).await
408}
409
410pub async fn del_typed<R, B>(
411    endpoint: &str,
412    options: Option<RequestOptions<B>>,
413) -> Result<R, ClobError>
414where
415    R: DeserializeOwned,
416    B: Serialize,
417{
418    let client = Client::new();
419    let mut req = client.delete(endpoint);
420    let mut debug_headers: Option<std::collections::HashMap<String, String>> = None;
421    let mut debug_body: Option<String> = None;
422    let mut debug_params: Option<QueryParams> = None;
423    if let Some(opts) = options {
424        if let Some(h) = opts.headers {
425            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
426                let mut masked = std::collections::HashMap::new();
427                for (k, v) in h.iter() {
428                    let key = k.to_string();
429                    let val = if key.contains("PASSPHRASE") || key.contains("API_KEY") {
430                        if v.len() > 6 {
431                            format!("{}***", &v[..6])
432                        } else {
433                            "***".to_string()
434                        }
435                    } else if key.contains("SIGNATURE") {
436                        if v.len() > 12 {
437                            format!("{}...", &v[..12])
438                        } else {
439                            "***".to_string()
440                        }
441                    } else {
442                        v.to_string()
443                    };
444                    masked.insert(key, val);
445                }
446                debug_headers = Some(masked);
447            }
448            for (k, v) in h.iter() {
449                req = req.header(k, v);
450            }
451        }
452        if let Some(body) = opts.data {
453            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
454                if let Ok(b) = serde_json::to_string(&body) {
455                    debug_body = Some(b);
456                }
457            }
458            req = req.json(&body);
459        }
460        if let Some(params) = opts.params {
461            if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
462                debug_params = Some(params.clone());
463            }
464            req = req.query(&params);
465        }
466    }
467    if std::env::var("CLOB_DEBUG_FULL").is_ok() || std::env::var("CLOB_DEBUG_RAW").is_ok() {
468        eprintln!("[HTTP DEBUG] DELETE {}", endpoint);
469        if let Some(h) = &debug_headers {
470            eprintln!("  headers={:?}", h);
471        }
472        if let Some(p) = &debug_params {
473            eprintln!("  params={:?}", p);
474        }
475        if let Some(b) = &debug_body {
476            let preview = if b.len() > 800 {
477                format!("{}... ({} bytes)", &b[..800], b.len())
478            } else {
479                b.clone()
480            };
481            eprintln!("  body={}", preview);
482        }
483    }
484    let resp = req
485        .send()
486        .await
487        .map_err(|e| ClobError::Other(format!("HTTP request failed: {}", e)))?;
488
489    let status = resp.status();
490
491    // Check status code first, before trying to parse
492    if !status.is_success() {
493        // Get response body as text for error details
494        let body_text = resp
495            .text()
496            .await
497            .unwrap_or_else(|_| "<unable to read response body>".to_string());
498
499        eprintln!("❌ HTTP Error Response:");
500        eprintln!("   Status: {}", status);
501        eprintln!("   Endpoint: {}", endpoint);
502        eprintln!("   Response Body: {}", body_text);
503
504        return Err(ClobError::Other(format!(
505            "HTTP {} error from {}: {}",
506            status, endpoint, body_text
507        )));
508    }
509
510    // Try to parse JSON, with detailed error message if it fails
511    let body_text = resp
512        .text()
513        .await
514        .map_err(|e| ClobError::Other(format!("Failed to read response body: {}", e)))?;
515
516    match serde_json::from_str::<R>(&body_text) {
517        Ok(val) => Ok(val),
518        Err(e) => {
519            eprintln!("❌ JSON Parse Error:");
520            eprintln!("   Endpoint: {}", endpoint);
521            eprintln!("   Error: {}", e);
522            eprintln!("   Response Body: {}", body_text);
523
524            Err(ClobError::Other(format!(
525                "Failed to parse JSON response from {}: {}. Response body: {}",
526                endpoint,
527                e,
528                if body_text.len() > 500 {
529                    format!(
530                        "{}... (truncated, {} bytes total)",
531                        &body_text[..500],
532                        body_text.len()
533                    )
534                } else {
535                    body_text
536                }
537            )))
538        }
539    }
540}
541
542pub async fn del(endpoint: &str, options: Option<RequestOptions>) -> Result<Value, ClobError> {
543    del_typed::<Value, Value>(endpoint, options).await
544}
545
546pub fn parse_orders_scoring_params(order_ids: Option<&Vec<String>>) -> QueryParams {
547    let mut params = QueryParams::new();
548    if let Some(ids) = order_ids {
549        params.insert("order_ids".to_string(), ids.join(","));
550    }
551    params
552}
553
554pub fn parse_drop_notification_params(ids: Option<&Vec<String>>) -> QueryParams {
555    let mut params = QueryParams::new();
556    if let Some(arr) = ids {
557        params.insert("ids".to_string(), arr.join(","));
558    }
559    params
560}