1use std::{
2 convert::Infallible,
3 error::Error,
4 fmt::Debug,
5 marker::PhantomData,
6 pin::Pin,
7 time::{Duration, Instant},
8};
9
10use eventsource_stream::{Event as SseEvent, Eventsource};
11use futures_util::{Stream, StreamExt};
12use longbridge_geo::{DC_REGION_HEADER, DcRegion, is_cn};
13use reqwest::{
14 Method, StatusCode,
15 header::{ACCEPT, HeaderMap, HeaderName, HeaderValue},
16};
17use serde::{Deserialize, Serialize, de::DeserializeOwned};
18
19use crate::{
20 AuthConfig, HttpClient, HttpClientError, HttpClientResult,
21 signature::{SignatureParams, signature},
22 timestamp::Timestamp,
23};
24
25const HTTP_URL: &str = "https://openapi.longbridge.com";
26const HTTP_URL_CN: &str = "https://openapi.longbridge.cn";
27
28const USER_AGENT: &str = "openapi-sdk";
29const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
30const RETRY_COUNT: usize = 5;
31const RETRY_INITIAL_DELAY: Duration = Duration::from_millis(100);
32const RETRY_FACTOR: f32 = 2.0;
33
34#[derive(Debug)]
36pub struct Json<T>(pub T);
37
38pub trait FromPayload: Sized + Send + Sync + 'static {
40 type Err: Error;
42
43 fn parse_from_bytes(data: &[u8]) -> Result<Self, Self::Err>;
45}
46
47pub trait ToPayload: Debug + Sized + Send + Sync + 'static {
49 type Err: Error;
51
52 fn to_bytes(&self) -> Result<Vec<u8>, Self::Err>;
54}
55
56impl<T> FromPayload for Json<T>
57where
58 T: DeserializeOwned + Send + Sync + 'static,
59{
60 type Err = serde_json::Error;
61
62 #[inline]
63 fn parse_from_bytes(data: &[u8]) -> Result<Self, Self::Err> {
64 Ok(Json(serde_json::from_slice(data)?))
65 }
66}
67
68impl<T> ToPayload for Json<T>
69where
70 T: Debug + Serialize + Send + Sync + 'static,
71{
72 type Err = serde_json::Error;
73
74 #[inline]
75 fn to_bytes(&self) -> Result<Vec<u8>, Self::Err> {
76 serde_json::to_vec(&self.0)
77 }
78}
79
80impl FromPayload for String {
81 type Err = std::string::FromUtf8Error;
82
83 #[inline]
84 fn parse_from_bytes(data: &[u8]) -> Result<Self, Self::Err> {
85 String::from_utf8(data.to_vec())
86 }
87}
88
89impl ToPayload for String {
90 type Err = std::string::FromUtf8Error;
91
92 #[inline]
93 fn to_bytes(&self) -> Result<Vec<u8>, Self::Err> {
94 Ok(self.clone().into_bytes())
95 }
96}
97
98impl FromPayload for () {
99 type Err = Infallible;
100
101 #[inline]
102 fn parse_from_bytes(_data: &[u8]) -> Result<Self, Self::Err> {
103 Ok(())
104 }
105}
106
107impl ToPayload for () {
108 type Err = Infallible;
109
110 #[inline]
111 fn to_bytes(&self) -> Result<Vec<u8>, Self::Err> {
112 Ok(vec![])
113 }
114}
115
116#[derive(Deserialize)]
117struct OpenApiResponse {
118 code: i32,
119 message: String,
120 data: Option<Box<serde_json::value::RawValue>>,
121}
122
123pub struct RequestBuilder<'a, T, Q, R> {
125 client: &'a HttpClient,
126 method: Method,
127 path: String,
128 headers: HeaderMap,
129 body: Option<T>,
130 query_params: Option<Q>,
131 dc_restrict: Option<DcRegion>,
132 timeout: Option<Duration>,
133 mark_resp: PhantomData<R>,
134}
135
136impl<'a> RequestBuilder<'a, (), (), ()> {
137 pub(crate) fn new(client: &'a HttpClient, method: Method, path: impl Into<String>) -> Self {
138 Self {
139 client,
140 method,
141 path: path.into(),
142 headers: Default::default(),
143 body: None,
144 query_params: None,
145 dc_restrict: None,
146 timeout: None,
147 mark_resp: PhantomData,
148 }
149 }
150}
151
152impl<'a, T, Q, R> RequestBuilder<'a, T, Q, R> {
153 #[must_use]
155 pub fn body<T2>(self, body: T2) -> RequestBuilder<'a, T2, Q, R>
156 where
157 T2: ToPayload,
158 {
159 RequestBuilder {
160 client: self.client,
161 method: self.method,
162 path: self.path,
163 headers: self.headers,
164 body: Some(body),
165 query_params: self.query_params,
166 dc_restrict: self.dc_restrict,
167 timeout: self.timeout,
168 mark_resp: self.mark_resp,
169 }
170 }
171
172 #[must_use]
174 pub fn header<K, V>(mut self, key: K, value: V) -> Self
175 where
176 K: TryInto<HeaderName>,
177 V: TryInto<HeaderValue>,
178 {
179 let key = key.try_into();
180 let value = value.try_into();
181 if let (Ok(key), Ok(value)) = (key, value) {
182 self.headers.insert(key, value);
183 }
184 self
185 }
186
187 #[must_use]
195 pub fn dc_restrict(mut self, region: DcRegion) -> Self {
196 self.dc_restrict = Some(region);
197 self
198 }
199
200 #[must_use]
206 pub fn timeout(mut self, timeout: Duration) -> Self {
207 self.timeout = Some(timeout);
208 self
209 }
210
211 #[must_use]
213 pub fn query_params<Q2>(self, params: Q2) -> RequestBuilder<'a, T, Q2, R>
214 where
215 Q2: Serialize + Send + Sync,
216 {
217 RequestBuilder {
218 client: self.client,
219 method: self.method,
220 path: self.path,
221 headers: self.headers,
222 body: self.body,
223 query_params: Some(params),
224 dc_restrict: self.dc_restrict,
225 timeout: self.timeout,
226 mark_resp: self.mark_resp,
227 }
228 }
229
230 #[must_use]
232 pub fn response<R2>(self) -> RequestBuilder<'a, T, Q, R2>
233 where
234 R2: FromPayload,
235 {
236 RequestBuilder {
237 client: self.client,
238 method: self.method,
239 path: self.path,
240 headers: self.headers,
241 body: self.body,
242 query_params: self.query_params,
243 dc_restrict: self.dc_restrict,
244 timeout: self.timeout,
245 mark_resp: PhantomData,
246 }
247 }
248}
249
250fn parse_response_envelope(
256 status: StatusCode,
257 trace_id: &str,
258 text: &str,
259) -> HttpClientResult<Box<serde_json::value::RawValue>> {
260 match serde_json::from_str::<OpenApiResponse>(text) {
261 Ok(resp) if resp.code == 0 => resp.data.ok_or(HttpClientError::UnexpectedResponse),
262 Ok(resp) => Err(HttpClientError::OpenApi {
263 code: resp.code,
264 message: resp.message,
265 trace_id: trace_id.to_string(),
266 }),
267 Err(err) if status == StatusCode::OK => {
268 Err(HttpClientError::DeserializeResponseBody(err.to_string()))
269 }
270 Err(_) => Err(HttpClientError::BadStatus(status)),
271 }
272}
273
274impl<T, Q, R> RequestBuilder<'_, T, Q, R>
275where
276 T: ToPayload,
277 Q: Serialize + Send,
278{
279 async fn http_url(&self) -> &str {
280 if let Some(url) = self.client.config.http_url.as_deref() {
281 return url;
282 }
283
284 if is_cn().await { HTTP_URL_CN } else { HTTP_URL }
285 }
286
287 async fn build_request(&self) -> HttpClientResult<reqwest::Request> {
291 let HttpClient {
292 http_cli,
293 config,
294 default_headers,
295 } = &self.client;
296 let timestamp = self
297 .headers
298 .get("X-Timestamp")
299 .and_then(|value| value.to_str().ok())
300 .and_then(|value| value.parse().ok())
301 .unwrap_or_else(Timestamp::now);
302
303 let (app_key, access_token, app_secret, dc_region) = match &config.auth {
306 AuthConfig::ApiKey {
307 app_key,
308 app_secret,
309 access_token,
310 } => (
311 app_key.clone(),
312 access_token.clone(),
313 Some(app_secret.clone()),
314 DcRegion::from_credentials(&[app_key, access_token, app_secret]),
315 ),
316 AuthConfig::OAuth(oauth) => {
317 let token = oauth
318 .access_token()
319 .await
320 .map_err(|e| HttpClientError::OAuth(e.to_string()))?;
321 let region = DcRegion::from_credential(&token);
325 (
326 oauth.client_id().to_string(),
327 format!("Bearer {token}"),
328 None,
329 region,
330 )
331 }
332 };
333
334 if let Some(required) = self.dc_restrict
337 && !dc_region.allows(required)
338 {
339 return Err(HttpClientError::DcRegionRestricted {
340 path: self.path.clone(),
341 required,
342 current: dc_region,
343 });
344 }
345
346 let app_key_value =
347 HeaderValue::from_str(&app_key).map_err(|_| HttpClientError::InvalidApiKey)?;
348 let access_token_value = HeaderValue::from_str(&access_token)
349 .map_err(|_| HttpClientError::InvalidAccessToken)?;
350
351 let url = self.http_url().await;
352 let mut request_builder = http_cli
353 .request(self.method.clone(), format!("{}{}", url, self.path))
354 .headers(default_headers.clone())
355 .headers(self.headers.clone())
356 .header("User-Agent", USER_AGENT)
357 .header("X-Api-Key", app_key_value)
358 .header("Authorization", access_token_value)
359 .header("X-Timestamp", timestamp.to_string())
360 .header("Content-Type", "application/json; charset=utf-8");
361
362 let region_already_set = default_headers.contains_key(DC_REGION_HEADER)
366 || self.headers.contains_key(DC_REGION_HEADER);
367 if !region_already_set {
368 request_builder = request_builder.header(DC_REGION_HEADER, dc_region.as_str());
369 }
370
371 if let Some(body) = &self.body {
373 let body = body
374 .to_bytes()
375 .map_err(|err| HttpClientError::SerializeRequestBody(err.to_string()))?;
376 request_builder = request_builder.body(body);
377 }
378
379 let mut request = request_builder.build().expect("invalid request");
380
381 if let Some(query_params) = &self.query_params {
383 let query_string = crate::qs::to_string(&query_params)?;
384 request.url_mut().set_query(Some(&query_string));
385 }
386
387 if let Some(secret) = app_secret {
389 let sign = signature(SignatureParams {
390 request: &request,
391 app_key: &app_key,
392 access_token: Some(&access_token),
393 app_secret: &secret,
394 timestamp,
395 });
396 if let Some(signature_value) = sign {
397 request.headers_mut().insert(
398 "X-Api-Signature",
399 HeaderValue::from_maybe_shared(signature_value).expect("valid signature"),
400 );
401 }
402 }
403
404 if let Some(body) = &self.body {
405 tracing::info!(method = %request.method(), url = %request.url(), body = ?body, "http request");
406 } else {
407 tracing::info!(method = %request.method(), url = %request.url(), "http request");
408 }
409
410 Ok(request)
411 }
412}
413
414impl<T, Q, R> RequestBuilder<'_, T, Q, R>
415where
416 T: ToPayload,
417 Q: Serialize + Send,
418 R: FromPayload,
419{
420 async fn do_send(&self) -> HttpClientResult<R> {
421 let http_cli = &self.client.http_cli;
422 let request = self.build_request().await?;
423
424 let s = Instant::now();
425 let timeout = self.timeout.unwrap_or(REQUEST_TIMEOUT);
426
427 let (status, trace_id, headers, text) = tokio::time::timeout(timeout, async move {
429 let resp = http_cli
430 .execute(request)
431 .await
432 .map_err(|err| HttpClientError::Http(err.into()))?;
433 let status = resp.status();
434 let headers = resp.headers().clone();
435 let trace_id = resp
436 .headers()
437 .get("x-trace-id")
438 .and_then(|value| value.to_str().ok())
439 .unwrap_or_default()
440 .to_string();
441 let text = resp
442 .text()
443 .await
444 .map_err(|err| HttpClientError::Http(err.into()))?;
445 Ok::<_, HttpClientError>((status, trace_id, headers, text))
446 })
447 .await
448 .map_err(|_| HttpClientError::RequestTimeout)??;
449
450 tracing::info!(duration = ?s.elapsed(), body = %text.as_str(), "http response");
451
452 let data = match serde_json::from_str::<OpenApiResponse>(&text) {
453 Ok(resp) if resp.code == 0 => resp.data.ok_or(HttpClientError::UnexpectedResponse),
454 Ok(resp) => Err(HttpClientError::OpenApi {
455 code: resp.code,
456 message: resp.message,
457 trace_id,
458 }),
459 Err(err) if status == StatusCode::OK => {
460 Err(HttpClientError::DeserializeResponseBody(err.to_string()))
461 }
462 Err(_) => Err(HttpClientError::UnexpectedHttpResponse {
463 status,
464 trace_id,
465 headers: Box::new(headers),
466 body: text,
467 }),
468 }?;
469
470 R::parse_from_bytes(data.get().as_bytes())
471 .map_err(|err| HttpClientError::DeserializeResponseBody(err.to_string()))
472 }
473
474 pub async fn send(self) -> HttpClientResult<R> {
476 match self.do_send().await {
477 Ok(resp) => Ok(resp),
478 Err(err) if is_too_many_requests(&err) => {
479 let mut last_error = err;
480 let mut retry_delay = RETRY_INITIAL_DELAY;
481
482 for _ in 0..RETRY_COUNT {
483 tokio::time::sleep(retry_delay).await;
484
485 match self.do_send().await {
486 Ok(resp) => return Ok(resp),
487 Err(err) if is_too_many_requests(&err) => {
488 last_error = err;
489 retry_delay =
490 Duration::from_secs_f32(retry_delay.as_secs_f32() * RETRY_FACTOR);
491 continue;
492 }
493 Err(err) => return Err(err),
494 }
495 }
496
497 Err(last_error)
498 }
499 Err(err) => Err(err),
500 }
501 }
502}
503
504fn is_too_many_requests(err: &HttpClientError) -> bool {
505 matches!(
506 err,
507 HttpClientError::BadStatus(StatusCode::TOO_MANY_REQUESTS)
508 | HttpClientError::UnexpectedHttpResponse {
509 status: StatusCode::TOO_MANY_REQUESTS,
510 ..
511 }
512 )
513}
514
515impl<T, Q> RequestBuilder<'_, T, Q, ()>
516where
517 T: ToPayload,
518 Q: Serialize + Send,
519{
520 pub async fn send_events(
527 self,
528 ) -> HttpClientResult<Pin<Box<dyn Stream<Item = HttpClientResult<SseEvent>> + Send>>> {
529 let http_cli = self.client.http_cli.clone();
530 let timeout = self.timeout.unwrap_or(REQUEST_TIMEOUT);
531 let mut request = self.build_request().await?;
532 request
533 .headers_mut()
534 .insert(ACCEPT, HeaderValue::from_static("text/event-stream"));
535
536 let resp = tokio::time::timeout(timeout, http_cli.execute(request))
540 .await
541 .map_err(|_| HttpClientError::RequestTimeout)?
542 .map_err(|err| HttpClientError::Http(err.into()))?;
543 let status = resp.status();
544
545 if status != StatusCode::OK {
546 let trace_id = resp
548 .headers()
549 .get("x-trace-id")
550 .and_then(|value| value.to_str().ok())
551 .unwrap_or_default()
552 .to_string();
553 let text = resp
554 .text()
555 .await
556 .map_err(|err| HttpClientError::Http(err.into()))?;
557 return Err(match parse_response_envelope(status, &trace_id, &text) {
558 Ok(_) => HttpClientError::UnexpectedResponse,
559 Err(err) => err,
560 });
561 }
562
563 let stream = resp.bytes_stream().eventsource().map(|item| {
564 item.map_err(|err| match err {
565 eventsource_stream::EventStreamError::Transport(err) => {
566 HttpClientError::Http(err.into())
567 }
568 err => HttpClientError::Sse(err.to_string()),
569 })
570 });
571 Ok(Box::pin(stream))
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use reqwest::{StatusCode, header::HeaderMap};
578
579 use super::is_too_many_requests;
580 use crate::HttpClientError;
581
582 #[test]
583 fn unexpected_http_response_preserves_original_context() {
584 let mut headers = HeaderMap::new();
585 headers.insert("server", "awselb/2.0".parse().unwrap());
586 let body = "<html><body>Too many IPs in X-Forwarded-For header.</body></html>";
587 let err = HttpClientError::UnexpectedHttpResponse {
588 status: StatusCode::from_u16(463).unwrap(),
589 trace_id: "trace-463".to_string(),
590 headers: Box::new(headers),
591 body: body.to_string(),
592 };
593
594 let HttpClientError::UnexpectedHttpResponse {
595 status,
596 trace_id,
597 headers,
598 body: preserved_body,
599 } = &err
600 else {
601 panic!("unexpected error variant");
602 };
603 assert_eq!(status.as_u16(), 463);
604 assert_eq!(trace_id, "trace-463");
605 assert_eq!(headers["server"], "awselb/2.0");
606 assert_eq!(preserved_body, body);
607 assert_eq!(
608 err.to_string(),
609 format!(
610 "unexpected HTTP response: status=463 <unknown status code>, trace_id=trace-463, body={body}"
611 )
612 );
613 }
614
615 #[test]
616 fn rich_rate_limit_response_remains_retryable() {
617 let err = HttpClientError::UnexpectedHttpResponse {
618 status: StatusCode::TOO_MANY_REQUESTS,
619 trace_id: String::new(),
620 headers: Box::new(HeaderMap::new()),
621 body: "rate limited".to_string(),
622 };
623
624 assert!(is_too_many_requests(&err));
625 }
626}