Skip to main content

longbridge_httpcli/
request.rs

1use std::{
2    convert::Infallible,
3    error::Error,
4    fmt::Debug,
5    marker::PhantomData,
6    time::{Duration, Instant},
7};
8
9use reqwest::{
10    Method, StatusCode,
11    header::{HeaderMap, HeaderName, HeaderValue},
12};
13use serde::{Deserialize, Serialize, de::DeserializeOwned};
14
15use crate::{
16    AuthConfig, HttpClient, HttpClientError, HttpClientResult, is_cn,
17    signature::{SignatureParams, signature},
18    timestamp::Timestamp,
19};
20
21const HTTP_URL: &str = "https://openapi.longbridge.com";
22const HTTP_URL_CN: &str = "https://openapi.longportapp.cn";
23
24const USER_AGENT: &str = "openapi-sdk";
25const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
26const RETRY_COUNT: usize = 5;
27const RETRY_INITIAL_DELAY: Duration = Duration::from_millis(100);
28const RETRY_FACTOR: f32 = 2.0;
29
30/// A JSON payload
31#[derive(Debug)]
32pub struct Json<T>(pub T);
33
34/// Represents a type that can parse from payload
35pub trait FromPayload: Sized + Send + Sync + 'static {
36    /// A error type
37    type Err: Error;
38
39    /// Parse the payload to this object
40    fn parse_from_bytes(data: &[u8]) -> Result<Self, Self::Err>;
41}
42
43/// Represents a type that can convert to payload
44pub trait ToPayload: Debug + Sized + Send + Sync + 'static {
45    /// A error type
46    type Err: Error;
47
48    /// Convert this object to the payload
49    fn to_bytes(&self) -> Result<Vec<u8>, Self::Err>;
50}
51
52impl<T> FromPayload for Json<T>
53where
54    T: DeserializeOwned + Send + Sync + 'static,
55{
56    type Err = serde_json::Error;
57
58    #[inline]
59    fn parse_from_bytes(data: &[u8]) -> Result<Self, Self::Err> {
60        Ok(Json(serde_json::from_slice(data)?))
61    }
62}
63
64impl<T> ToPayload for Json<T>
65where
66    T: Debug + Serialize + Send + Sync + 'static,
67{
68    type Err = serde_json::Error;
69
70    #[inline]
71    fn to_bytes(&self) -> Result<Vec<u8>, Self::Err> {
72        serde_json::to_vec(&self.0)
73    }
74}
75
76impl FromPayload for String {
77    type Err = std::string::FromUtf8Error;
78
79    #[inline]
80    fn parse_from_bytes(data: &[u8]) -> Result<Self, Self::Err> {
81        String::from_utf8(data.to_vec())
82    }
83}
84
85impl ToPayload for String {
86    type Err = std::string::FromUtf8Error;
87
88    #[inline]
89    fn to_bytes(&self) -> Result<Vec<u8>, Self::Err> {
90        Ok(self.clone().into_bytes())
91    }
92}
93
94impl FromPayload for () {
95    type Err = Infallible;
96
97    #[inline]
98    fn parse_from_bytes(_data: &[u8]) -> Result<Self, Self::Err> {
99        Ok(())
100    }
101}
102
103impl ToPayload for () {
104    type Err = Infallible;
105
106    #[inline]
107    fn to_bytes(&self) -> Result<Vec<u8>, Self::Err> {
108        Ok(vec![])
109    }
110}
111
112#[derive(Deserialize)]
113struct OpenApiResponse {
114    code: i32,
115    message: String,
116    data: Option<Box<serde_json::value::RawValue>>,
117}
118
119/// A request builder
120pub struct RequestBuilder<'a, T, Q, R> {
121    client: &'a HttpClient,
122    method: Method,
123    path: String,
124    headers: HeaderMap,
125    body: Option<T>,
126    query_params: Option<Q>,
127    mark_resp: PhantomData<R>,
128}
129
130impl<'a> RequestBuilder<'a, (), (), ()> {
131    pub(crate) fn new(client: &'a HttpClient, method: Method, path: impl Into<String>) -> Self {
132        Self {
133            client,
134            method,
135            path: path.into(),
136            headers: Default::default(),
137            body: None,
138            query_params: None,
139            mark_resp: PhantomData,
140        }
141    }
142}
143
144impl<'a, T, Q, R> RequestBuilder<'a, T, Q, R> {
145    /// Set the request body
146    #[must_use]
147    pub fn body<T2>(self, body: T2) -> RequestBuilder<'a, T2, Q, R>
148    where
149        T2: ToPayload,
150    {
151        RequestBuilder {
152            client: self.client,
153            method: self.method,
154            path: self.path,
155            headers: self.headers,
156            body: Some(body),
157            query_params: self.query_params,
158            mark_resp: self.mark_resp,
159        }
160    }
161
162    /// Set the header
163    #[must_use]
164    pub fn header<K, V>(mut self, key: K, value: V) -> Self
165    where
166        K: TryInto<HeaderName>,
167        V: TryInto<HeaderValue>,
168    {
169        let key = key.try_into();
170        let value = value.try_into();
171        if let (Ok(key), Ok(value)) = (key, value) {
172            self.headers.insert(key, value);
173        }
174        self
175    }
176
177    /// Set the query string
178    #[must_use]
179    pub fn query_params<Q2>(self, params: Q2) -> RequestBuilder<'a, T, Q2, R>
180    where
181        Q2: Serialize + Send + Sync,
182    {
183        RequestBuilder {
184            client: self.client,
185            method: self.method,
186            path: self.path,
187            headers: self.headers,
188            body: self.body,
189            query_params: Some(params),
190            mark_resp: self.mark_resp,
191        }
192    }
193
194    /// Set the response body type
195    #[must_use]
196    pub fn response<R2>(self) -> RequestBuilder<'a, T, Q, R2>
197    where
198        R2: FromPayload,
199    {
200        RequestBuilder {
201            client: self.client,
202            method: self.method,
203            path: self.path,
204            headers: self.headers,
205            body: self.body,
206            query_params: self.query_params,
207            mark_resp: PhantomData,
208        }
209    }
210}
211
212impl<T, Q, R> RequestBuilder<'_, T, Q, R>
213where
214    T: ToPayload,
215    Q: Serialize + Send,
216    R: FromPayload,
217{
218    async fn http_url(&self) -> &str {
219        if let Some(url) = self.client.config.http_url.as_deref() {
220            return url;
221        }
222
223        if is_cn().await { HTTP_URL_CN } else { HTTP_URL }
224    }
225
226    async fn do_send(&self) -> HttpClientResult<R> {
227        let HttpClient {
228            http_cli,
229            config,
230            default_headers,
231        } = &self.client;
232        let timestamp = self
233            .headers
234            .get("X-Timestamp")
235            .and_then(|value| value.to_str().ok())
236            .and_then(|value| value.parse().ok())
237            .unwrap_or_else(Timestamp::now);
238
239        // Resolve app_key, access_token, and optional app_secret from auth config
240        let (app_key, access_token, app_secret) = match &config.auth {
241            AuthConfig::ApiKey {
242                app_key,
243                app_secret,
244                access_token,
245            } => (
246                app_key.clone(),
247                access_token.clone(),
248                Some(app_secret.clone()),
249            ),
250            AuthConfig::OAuth(oauth) => {
251                let token = oauth
252                    .access_token()
253                    .await
254                    .map_err(|e| HttpClientError::OAuth(e.to_string()))?;
255                (
256                    oauth.client_id().to_string(),
257                    format!("Bearer {token}"),
258                    None,
259                )
260            }
261        };
262
263        let app_key_value =
264            HeaderValue::from_str(&app_key).map_err(|_| HttpClientError::InvalidApiKey)?;
265        let access_token_value = HeaderValue::from_str(&access_token)
266            .map_err(|_| HttpClientError::InvalidAccessToken)?;
267
268        let url = self.http_url().await;
269        let mut request_builder = http_cli
270            .request(self.method.clone(), format!("{}{}", url, self.path))
271            .headers(default_headers.clone())
272            .headers(self.headers.clone())
273            .header("User-Agent", USER_AGENT)
274            .header("X-Api-Key", app_key_value)
275            .header("Authorization", access_token_value)
276            .header("X-Timestamp", timestamp.to_string())
277            .header("Content-Type", "application/json; charset=utf-8");
278
279        // set the request body
280        if let Some(body) = &self.body {
281            let body = body
282                .to_bytes()
283                .map_err(|err| HttpClientError::SerializeRequestBody(err.to_string()))?;
284            request_builder = request_builder.body(body);
285        }
286
287        let mut request = request_builder.build().expect("invalid request");
288
289        // set the query string
290        if let Some(query_params) = &self.query_params {
291            let query_string = crate::qs::to_string(&query_params)?;
292            request.url_mut().set_query(Some(&query_string));
293        }
294
295        // Generate HMAC-SHA256 signature only for ApiKey mode
296        if let Some(secret) = app_secret {
297            let sign = signature(SignatureParams {
298                request: &request,
299                app_key: &app_key,
300                access_token: Some(&access_token),
301                app_secret: &secret,
302                timestamp,
303            });
304            if let Some(signature_value) = sign {
305                request.headers_mut().insert(
306                    "X-Api-Signature",
307                    HeaderValue::from_maybe_shared(signature_value).expect("valid signature"),
308                );
309            }
310        }
311
312        if let Some(body) = &self.body {
313            tracing::info!(method = %request.method(), url = %request.url(), body = ?body, "http request");
314        } else {
315            tracing::info!(method = %request.method(), url = %request.url(), "http request");
316        }
317
318        let s = Instant::now();
319
320        // send request
321        let (status, trace_id, text) = tokio::time::timeout(REQUEST_TIMEOUT, async move {
322            let resp = http_cli
323                .execute(request)
324                .await
325                .map_err(|err| HttpClientError::Http(err.into()))?;
326            let status = resp.status();
327            let trace_id = resp
328                .headers()
329                .get("x-trace-id")
330                .and_then(|value| value.to_str().ok())
331                .unwrap_or_default()
332                .to_string();
333            let text = resp
334                .text()
335                .await
336                .map_err(|err| HttpClientError::Http(err.into()))?;
337            Ok::<_, HttpClientError>((status, trace_id, text))
338        })
339        .await
340        .map_err(|_| HttpClientError::RequestTimeout)??;
341
342        tracing::info!(duration = ?s.elapsed(), body = %text.as_str(), "http response");
343
344        let resp = match serde_json::from_str::<OpenApiResponse>(&text) {
345            Ok(resp) if resp.code == 0 => resp.data.ok_or(HttpClientError::UnexpectedResponse),
346            Ok(resp) => Err(HttpClientError::OpenApi {
347                code: resp.code,
348                message: resp.message,
349                trace_id,
350            }),
351            Err(err) if status == StatusCode::OK => {
352                Err(HttpClientError::DeserializeResponseBody(err.to_string()))
353            }
354            Err(_) => Err(HttpClientError::BadStatus(status)),
355        }?;
356
357        R::parse_from_bytes(resp.get().as_bytes())
358            .map_err(|err| HttpClientError::DeserializeResponseBody(err.to_string()))
359    }
360
361    /// Send request and get the response
362    pub async fn send(self) -> HttpClientResult<R> {
363        match self.do_send().await {
364            Ok(resp) => Ok(resp),
365            Err(HttpClientError::BadStatus(StatusCode::TOO_MANY_REQUESTS)) => {
366                let mut retry_delay = RETRY_INITIAL_DELAY;
367
368                for _ in 0..RETRY_COUNT {
369                    tokio::time::sleep(retry_delay).await;
370
371                    match self.do_send().await {
372                        Ok(resp) => return Ok(resp),
373                        Err(HttpClientError::BadStatus(StatusCode::TOO_MANY_REQUESTS)) => {
374                            retry_delay =
375                                Duration::from_secs_f32(retry_delay.as_secs_f32() * RETRY_FACTOR);
376                            continue;
377                        }
378                        Err(err) => return Err(err),
379                    }
380                }
381
382                Err(HttpClientError::BadStatus(StatusCode::TOO_MANY_REQUESTS))
383            }
384            Err(err) => Err(err),
385        }
386    }
387}