Skip to main content

longbridge_httpcli/
client.rs

1use std::sync::{Arc, LazyLock};
2
3use longbridge_geo::DcRegion;
4use reqwest::{
5    Client, Method,
6    header::{HeaderMap, HeaderName, HeaderValue},
7};
8use serde::Deserialize;
9
10use crate::{
11    AuthConfig, HttpClientConfig, HttpClientError, HttpClientResult, Json, RequestBuilder,
12};
13
14/// Process-wide shared `reqwest::Client`.
15///
16/// `reqwest::Client` is internally reference-counted and owns the connection
17/// pool, DNS cache and TLS state; cloning it is cheap and shares that pool.
18/// Every SDK context builds its own [`HttpClient`], so a process that churns
19/// thousands of contexts would otherwise spin up thousands of independent
20/// connection pools. All requests target the same OpenAPI host and auth is
21/// applied per-request, so a single shared client is both correct and far
22/// cheaper.
23static SHARED_HTTP_CLIENT: LazyLock<Client> = LazyLock::new(Client::new);
24
25/// Longbridge HTTP client
26#[derive(Clone)]
27pub struct HttpClient {
28    pub(crate) http_cli: Client,
29    pub(crate) config: Arc<HttpClientConfig>,
30    pub(crate) default_headers: HeaderMap,
31}
32
33impl HttpClient {
34    /// Create a new `HttpClient`
35    pub fn new(config: HttpClientConfig) -> Self {
36        Self {
37            http_cli: SHARED_HTTP_CLIENT.clone(),
38            config: Arc::new(config),
39            default_headers: HeaderMap::new(),
40        }
41    }
42
43    /// Set the default header
44    pub fn header<K, V>(mut self, key: K, value: V) -> Self
45    where
46        K: TryInto<HeaderName>,
47        V: TryInto<HeaderValue>,
48    {
49        let key = key.try_into();
50        let value = value.try_into();
51        if let (Ok(key), Ok(value)) = (key, value) {
52            self.default_headers.insert(key, value);
53        }
54        self
55    }
56
57    /// The data-center region (`us`/`ap`) derived from this client's auth
58    /// credentials. Used by non-HTTP call sites (e.g. WebSocket quote commands)
59    /// to apply the same AP-only routing guard the HTTP path enforces.
60    pub async fn dc_region(&self) -> DcRegion {
61        match &self.config.auth {
62            AuthConfig::ApiKey {
63                app_key,
64                app_secret,
65                access_token,
66            } => DcRegion::from_credentials(&[app_key, access_token, app_secret]),
67            AuthConfig::OAuth(oauth) => oauth
68                .access_token()
69                .await
70                .map(|token| DcRegion::from_credential(&token))
71                .unwrap_or(DcRegion::Ap),
72        }
73    }
74
75    /// Create a new request builder
76    #[inline]
77    pub fn request(
78        &self,
79        method: Method,
80        path: impl Into<String>,
81    ) -> RequestBuilder<'_, (), (), ()> {
82        RequestBuilder::new(self, method, path)
83    }
84
85    /// Get the socket OTP(One Time Password)
86    ///
87    /// Reference: <https://open.longbridge.com/en/docs/socket-token-api>
88    pub async fn get_otp(&self) -> HttpClientResult<String> {
89        #[derive(Debug, Deserialize)]
90        struct Response {
91            otp: String,
92            limit: i32,
93            online: i32,
94        }
95
96        let resp = self
97            .request(Method::GET, "/v1/socket/token")
98            .response::<Json<Response>>()
99            .send()
100            .await?
101            .0;
102        tracing::info!(limit = resp.limit, online = resp.online, "create otp");
103
104        if resp.online >= resp.limit {
105            return Err(HttpClientError::ConnectionLimitExceeded {
106                limit: resp.limit,
107                online: resp.online,
108            });
109        }
110
111        Ok(resp.otp)
112    }
113}