Skip to main content

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