1use std::{
2 convert::Infallible,
3 error::Error,
4 fmt::Debug,
5 marker::PhantomData,
6 time::{Duration, Instant},
7};
8
9use longbridge_geo::{DC_REGION_HEADER, DcRegion, is_cn};
10use reqwest::{
11 Method, StatusCode,
12 header::{HeaderMap, HeaderName, HeaderValue},
13};
14use serde::{Deserialize, Serialize, de::DeserializeOwned};
15
16use crate::{
17 AuthConfig, HttpClient, HttpClientError, HttpClientResult,
18 signature::{SignatureParams, signature},
19 timestamp::Timestamp,
20};
21
22const HTTP_URL: &str = "https://openapi.longbridge.com";
23const HTTP_URL_CN: &str = "https://openapi.longbridge.cn";
24
25const USER_AGENT: &str = "openapi-sdk";
26const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
27const RETRY_COUNT: usize = 5;
28const RETRY_INITIAL_DELAY: Duration = Duration::from_millis(100);
29const RETRY_FACTOR: f32 = 2.0;
30
31#[derive(Debug)]
33pub struct Json<T>(pub T);
34
35pub trait FromPayload: Sized + Send + Sync + 'static {
37 type Err: Error;
39
40 fn parse_from_bytes(data: &[u8]) -> Result<Self, Self::Err>;
42}
43
44pub trait ToPayload: Debug + Sized + Send + Sync + 'static {
46 type Err: Error;
48
49 fn to_bytes(&self) -> Result<Vec<u8>, Self::Err>;
51}
52
53impl<T> FromPayload for Json<T>
54where
55 T: DeserializeOwned + Send + Sync + 'static,
56{
57 type Err = serde_json::Error;
58
59 #[inline]
60 fn parse_from_bytes(data: &[u8]) -> Result<Self, Self::Err> {
61 Ok(Json(serde_json::from_slice(data)?))
62 }
63}
64
65impl<T> ToPayload for Json<T>
66where
67 T: Debug + Serialize + Send + Sync + 'static,
68{
69 type Err = serde_json::Error;
70
71 #[inline]
72 fn to_bytes(&self) -> Result<Vec<u8>, Self::Err> {
73 serde_json::to_vec(&self.0)
74 }
75}
76
77impl FromPayload for String {
78 type Err = std::string::FromUtf8Error;
79
80 #[inline]
81 fn parse_from_bytes(data: &[u8]) -> Result<Self, Self::Err> {
82 String::from_utf8(data.to_vec())
83 }
84}
85
86impl ToPayload for String {
87 type Err = std::string::FromUtf8Error;
88
89 #[inline]
90 fn to_bytes(&self) -> Result<Vec<u8>, Self::Err> {
91 Ok(self.clone().into_bytes())
92 }
93}
94
95impl FromPayload for () {
96 type Err = Infallible;
97
98 #[inline]
99 fn parse_from_bytes(_data: &[u8]) -> Result<Self, Self::Err> {
100 Ok(())
101 }
102}
103
104impl ToPayload for () {
105 type Err = Infallible;
106
107 #[inline]
108 fn to_bytes(&self) -> Result<Vec<u8>, Self::Err> {
109 Ok(vec![])
110 }
111}
112
113#[derive(Deserialize)]
114struct OpenApiResponse {
115 code: i32,
116 message: String,
117 data: Option<Box<serde_json::value::RawValue>>,
118}
119
120pub struct RequestBuilder<'a, T, Q, R> {
122 client: &'a HttpClient,
123 method: Method,
124 path: String,
125 headers: HeaderMap,
126 body: Option<T>,
127 query_params: Option<Q>,
128 dc_restrict: Option<DcRegion>,
129 mark_resp: PhantomData<R>,
130}
131
132impl<'a> RequestBuilder<'a, (), (), ()> {
133 pub(crate) fn new(client: &'a HttpClient, method: Method, path: impl Into<String>) -> Self {
134 Self {
135 client,
136 method,
137 path: path.into(),
138 headers: Default::default(),
139 body: None,
140 query_params: None,
141 dc_restrict: None,
142 mark_resp: PhantomData,
143 }
144 }
145}
146
147impl<'a, T, Q, R> RequestBuilder<'a, T, Q, R> {
148 #[must_use]
150 pub fn body<T2>(self, body: T2) -> RequestBuilder<'a, T2, Q, R>
151 where
152 T2: ToPayload,
153 {
154 RequestBuilder {
155 client: self.client,
156 method: self.method,
157 path: self.path,
158 headers: self.headers,
159 body: Some(body),
160 query_params: self.query_params,
161 dc_restrict: self.dc_restrict,
162 mark_resp: self.mark_resp,
163 }
164 }
165
166 #[must_use]
168 pub fn header<K, V>(mut self, key: K, value: V) -> Self
169 where
170 K: TryInto<HeaderName>,
171 V: TryInto<HeaderValue>,
172 {
173 let key = key.try_into();
174 let value = value.try_into();
175 if let (Ok(key), Ok(value)) = (key, value) {
176 self.headers.insert(key, value);
177 }
178 self
179 }
180
181 #[must_use]
189 pub fn dc_restrict(mut self, region: DcRegion) -> Self {
190 self.dc_restrict = Some(region);
191 self
192 }
193
194 #[must_use]
196 pub fn query_params<Q2>(self, params: Q2) -> RequestBuilder<'a, T, Q2, R>
197 where
198 Q2: Serialize + Send + Sync,
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: Some(params),
207 dc_restrict: self.dc_restrict,
208 mark_resp: self.mark_resp,
209 }
210 }
211
212 #[must_use]
214 pub fn response<R2>(self) -> RequestBuilder<'a, T, Q, R2>
215 where
216 R2: FromPayload,
217 {
218 RequestBuilder {
219 client: self.client,
220 method: self.method,
221 path: self.path,
222 headers: self.headers,
223 body: self.body,
224 query_params: self.query_params,
225 dc_restrict: self.dc_restrict,
226 mark_resp: PhantomData,
227 }
228 }
229}
230
231impl<T, Q, R> RequestBuilder<'_, T, Q, R>
232where
233 T: ToPayload,
234 Q: Serialize + Send,
235 R: FromPayload,
236{
237 async fn http_url(&self) -> &str {
238 if let Some(url) = self.client.config.http_url.as_deref() {
239 return url;
240 }
241
242 if is_cn().await { HTTP_URL_CN } else { HTTP_URL }
243 }
244
245 async fn do_send(&self) -> HttpClientResult<R> {
246 let HttpClient {
247 http_cli,
248 config,
249 default_headers,
250 } = &self.client;
251 let timestamp = self
252 .headers
253 .get("X-Timestamp")
254 .and_then(|value| value.to_str().ok())
255 .and_then(|value| value.parse().ok())
256 .unwrap_or_else(Timestamp::now);
257
258 let (app_key, access_token, app_secret, dc_region) = match &config.auth {
261 AuthConfig::ApiKey {
262 app_key,
263 app_secret,
264 access_token,
265 } => (
266 app_key.clone(),
267 access_token.clone(),
268 Some(app_secret.clone()),
269 DcRegion::from_credentials(&[app_key, access_token, app_secret]),
270 ),
271 AuthConfig::OAuth(oauth) => {
272 let token = oauth
273 .access_token()
274 .await
275 .map_err(|e| HttpClientError::OAuth(e.to_string()))?;
276 let region = DcRegion::from_credential(&token);
280 (
281 oauth.client_id().to_string(),
282 format!("Bearer {token}"),
283 None,
284 region,
285 )
286 }
287 };
288
289 if let Some(required) = self.dc_restrict
292 && !dc_region.allows(required)
293 {
294 return Err(HttpClientError::DcRegionRestricted {
295 path: self.path.clone(),
296 required,
297 current: dc_region,
298 });
299 }
300
301 let app_key_value =
302 HeaderValue::from_str(&app_key).map_err(|_| HttpClientError::InvalidApiKey)?;
303 let access_token_value = HeaderValue::from_str(&access_token)
304 .map_err(|_| HttpClientError::InvalidAccessToken)?;
305
306 let url = self.http_url().await;
307 let mut request_builder = http_cli
308 .request(self.method.clone(), format!("{}{}", url, self.path))
309 .headers(default_headers.clone())
310 .headers(self.headers.clone())
311 .header("User-Agent", USER_AGENT)
312 .header("X-Api-Key", app_key_value)
313 .header("Authorization", access_token_value)
314 .header("X-Timestamp", timestamp.to_string())
315 .header("Content-Type", "application/json; charset=utf-8");
316
317 let region_already_set = default_headers.contains_key(DC_REGION_HEADER)
321 || self.headers.contains_key(DC_REGION_HEADER);
322 if !region_already_set {
323 request_builder = request_builder.header(DC_REGION_HEADER, dc_region.as_str());
324 }
325
326 if let Some(body) = &self.body {
328 let body = body
329 .to_bytes()
330 .map_err(|err| HttpClientError::SerializeRequestBody(err.to_string()))?;
331 request_builder = request_builder.body(body);
332 }
333
334 let mut request = request_builder.build().expect("invalid request");
335
336 if let Some(query_params) = &self.query_params {
338 let query_string = crate::qs::to_string(&query_params)?;
339 request.url_mut().set_query(Some(&query_string));
340 }
341
342 if let Some(secret) = app_secret {
344 let sign = signature(SignatureParams {
345 request: &request,
346 app_key: &app_key,
347 access_token: Some(&access_token),
348 app_secret: &secret,
349 timestamp,
350 });
351 if let Some(signature_value) = sign {
352 request.headers_mut().insert(
353 "X-Api-Signature",
354 HeaderValue::from_maybe_shared(signature_value).expect("valid signature"),
355 );
356 }
357 }
358
359 if let Some(body) = &self.body {
360 tracing::info!(method = %request.method(), url = %request.url(), body = ?body, "http request");
361 } else {
362 tracing::info!(method = %request.method(), url = %request.url(), "http request");
363 }
364
365 let s = Instant::now();
366
367 let (status, trace_id, headers, text) = tokio::time::timeout(REQUEST_TIMEOUT, async move {
369 let resp = http_cli
370 .execute(request)
371 .await
372 .map_err(|err| HttpClientError::Http(err.into()))?;
373 let status = resp.status();
374 let headers = resp.headers().clone();
375 let trace_id = resp
376 .headers()
377 .get("x-trace-id")
378 .and_then(|value| value.to_str().ok())
379 .unwrap_or_default()
380 .to_string();
381 let text = resp
382 .text()
383 .await
384 .map_err(|err| HttpClientError::Http(err.into()))?;
385 Ok::<_, HttpClientError>((status, trace_id, headers, text))
386 })
387 .await
388 .map_err(|_| HttpClientError::RequestTimeout)??;
389
390 tracing::info!(duration = ?s.elapsed(), body = %text.as_str(), "http response");
391
392 let resp = match serde_json::from_str::<OpenApiResponse>(&text) {
393 Ok(resp) if resp.code == 0 => resp.data.ok_or(HttpClientError::UnexpectedResponse),
394 Ok(resp) => Err(HttpClientError::OpenApi {
395 code: resp.code,
396 message: resp.message,
397 trace_id,
398 }),
399 Err(err) if status == StatusCode::OK => {
400 Err(HttpClientError::DeserializeResponseBody(err.to_string()))
401 }
402 Err(_) => Err(HttpClientError::UnexpectedHttpResponse {
403 status,
404 trace_id,
405 headers: Box::new(headers),
406 body: text,
407 }),
408 }?;
409
410 R::parse_from_bytes(resp.get().as_bytes())
411 .map_err(|err| HttpClientError::DeserializeResponseBody(err.to_string()))
412 }
413
414 pub async fn send(self) -> HttpClientResult<R> {
416 match self.do_send().await {
417 Ok(resp) => Ok(resp),
418 Err(err) if is_too_many_requests(&err) => {
419 let mut last_error = err;
420 let mut retry_delay = RETRY_INITIAL_DELAY;
421
422 for _ in 0..RETRY_COUNT {
423 tokio::time::sleep(retry_delay).await;
424
425 match self.do_send().await {
426 Ok(resp) => return Ok(resp),
427 Err(err) if is_too_many_requests(&err) => {
428 last_error = err;
429 retry_delay =
430 Duration::from_secs_f32(retry_delay.as_secs_f32() * RETRY_FACTOR);
431 continue;
432 }
433 Err(err) => return Err(err),
434 }
435 }
436
437 Err(last_error)
438 }
439 Err(err) => Err(err),
440 }
441 }
442}
443
444fn is_too_many_requests(err: &HttpClientError) -> bool {
445 matches!(
446 err,
447 HttpClientError::BadStatus(StatusCode::TOO_MANY_REQUESTS)
448 | HttpClientError::UnexpectedHttpResponse {
449 status: StatusCode::TOO_MANY_REQUESTS,
450 ..
451 }
452 )
453}
454
455#[cfg(test)]
456mod tests {
457 use reqwest::{StatusCode, header::HeaderMap};
458
459 use super::is_too_many_requests;
460 use crate::HttpClientError;
461
462 #[test]
463 fn unexpected_http_response_preserves_original_context() {
464 let mut headers = HeaderMap::new();
465 headers.insert("server", "awselb/2.0".parse().unwrap());
466 let body = "<html><body>Too many IPs in X-Forwarded-For header.</body></html>";
467 let err = HttpClientError::UnexpectedHttpResponse {
468 status: StatusCode::from_u16(463).unwrap(),
469 trace_id: "trace-463".to_string(),
470 headers: Box::new(headers),
471 body: body.to_string(),
472 };
473
474 let HttpClientError::UnexpectedHttpResponse {
475 status,
476 trace_id,
477 headers,
478 body: preserved_body,
479 } = &err
480 else {
481 panic!("unexpected error variant");
482 };
483 assert_eq!(status.as_u16(), 463);
484 assert_eq!(trace_id, "trace-463");
485 assert_eq!(headers["server"], "awselb/2.0");
486 assert_eq!(preserved_body, body);
487 assert_eq!(
488 err.to_string(),
489 format!(
490 "unexpected HTTP response: status=463 <unknown status code>, trace_id=trace-463, body={body}"
491 )
492 );
493 }
494
495 #[test]
496 fn rich_rate_limit_response_remains_retryable() {
497 let err = HttpClientError::UnexpectedHttpResponse {
498 status: StatusCode::TOO_MANY_REQUESTS,
499 trace_id: String::new(),
500 headers: Box::new(HeaderMap::new()),
501 body: "rate limited".to_string(),
502 };
503
504 assert!(is_too_many_requests(&err));
505 }
506}