1use super::Error;
17use async_trait::async_trait;
18use axum::http::Method;
19use axum::http::header::HeaderMap;
20use axum::http::uri::Uri;
21use bytes::Bytes;
22use reqwest::Client as ReqwestClient;
23use reqwest::RequestBuilder;
24use scopeguard::defer;
25use serde::Serialize;
26use serde::de::DeserializeOwned;
27use std::collections::HashMap;
28use std::net::SocketAddr;
29use std::sync::atomic::{AtomicU32, Ordering};
30use std::time::Duration;
31use tibba_util::{Stopwatch, json_get};
32use tracing::info;
33type Result<T> = std::result::Result<T, Error>;
34
35const VERSION: &str = env!("CARGO_PKG_VERSION");
36
37static EMPTY_QUERY: Option<&[(&str, &str)]> = None;
39static EMPTY_BODY: Option<&[(&str, &str)]> = None;
40
41#[derive(Clone, Debug, Default)]
44pub struct Params<'a, Q, P>
45where
46 Q: Serialize + ?Sized,
47 P: Serialize + ?Sized,
48{
49 pub method: Method,
51 pub timeout: Option<Duration>,
53 pub query: Option<&'a Q>,
55 pub body: Option<&'a P>,
57 pub url: &'a str,
59}
60
61#[derive(Default, Clone, Debug)]
63pub struct HttpStats {
64 pub method: String, pub path: String, pub remote_addr: String, pub status: u16, pub content_length: usize, pub processing: u32, pub transfer: u32, pub serde: u32, pub total: u32, pub tls_version: String, pub tls_not_before: String, pub tls_not_after: String, }
77
78#[async_trait]
80pub trait HttpInterceptor: Send + Sync {
81 async fn fail(&self, _status: u16, _data: &Bytes) -> Result<()> {
83 Ok(())
84 }
85 async fn request(&self, req: RequestBuilder) -> Result<RequestBuilder> {
87 Ok(req)
88 }
89 async fn response(&self, data: Bytes) -> Result<Bytes> {
91 Ok(data)
92 }
93 async fn on_done(&self, _stats: &HttpStats, _err: Option<&Error>) -> Result<()> {
95 Ok(())
96 }
97}
98
99pub async fn handle_fail(service: &str, status: u16, data: &Bytes) -> Result<()> {
101 if status >= 400 {
102 let mut message = json_get(data, "message");
103 if message.is_empty() {
104 message = "unknown error".to_string();
105 }
106 return Err(Error::Common {
107 service: service.to_string(),
108 message,
109 });
110 }
111 Ok(())
112}
113
114pub struct CommonInterceptor {
116 service: String,
117}
118
119impl CommonInterceptor {
120 pub fn new(service: &str) -> CommonInterceptor {
121 CommonInterceptor {
122 service: service.to_string(),
123 }
124 }
125}
126
127#[async_trait]
128impl HttpInterceptor for CommonInterceptor {
129 async fn fail(&self, status: u16, data: &Bytes) -> Result<()> {
130 handle_fail(&self.service, status, data).await
131 }
132 async fn request(&self, req: RequestBuilder) -> Result<RequestBuilder> {
133 Ok(req)
134 }
135 async fn response(&self, data: Bytes) -> Result<Bytes> {
136 Ok(data)
137 }
138 async fn on_done(&self, stats: &HttpStats, err: Option<&Error>) -> Result<()> {
139 let error = err.map(ToString::to_string);
140 info!(
141 service = self.service,
142 method = stats.method,
143 path = stats.path,
144 status = stats.status,
145 remote_addr = stats.remote_addr,
146 content_length = stats.content_length,
147 processing = stats.processing,
148 transfer = stats.transfer,
149 serde = stats.serde,
150 total = stats.total,
151 error,
152 );
153 Ok(())
154 }
155}
156
157struct ClientConfig {
159 service: String, base_url: String, read_timeout: Option<Duration>, timeout: Option<Duration>, connect_timeout: Option<Duration>, pool_idle_timeout: Option<Duration>, pool_max_idle_per_host: usize, max_processing: Option<u32>, headers: Option<HeaderMap>, dns_overrides: Option<HashMap<String, Vec<SocketAddr>>>,
169 interceptors: Option<Vec<Box<dyn HttpInterceptor>>>, }
171
172pub struct ClientBuilder {
174 config: ClientConfig,
175}
176
177impl ClientBuilder {
178 pub fn new(service: &str) -> Self {
186 Self {
187 config: ClientConfig {
188 service: service.to_string(),
189 base_url: "".to_string(),
190 read_timeout: None,
191 timeout: None,
192 connect_timeout: None,
193 pool_idle_timeout: None,
194 pool_max_idle_per_host: 0,
195 headers: None,
196 interceptors: None,
197 max_processing: None,
198 dns_overrides: None,
199 },
200 }
201 }
202
203 pub fn with_base_url(mut self, base_url: &str) -> Self {
211 self.config.base_url = base_url.to_string();
212 self
213 }
214
215 pub fn with_interceptor(mut self, interceptor: Box<dyn HttpInterceptor>) -> Self {
223 self.config
224 .interceptors
225 .get_or_insert_with(Vec::new)
226 .push(interceptor);
227 self
228 }
229
230 pub fn with_timeout(mut self, timeout: Duration) -> Self {
238 self.config.timeout = Some(timeout);
239 self
240 }
241
242 pub fn with_read_timeout(mut self, read_timeout: Duration) -> Self {
250 self.config.read_timeout = Some(read_timeout);
251 self
252 }
253
254 pub fn with_connect_timeout(mut self, connect_timeout: Duration) -> Self {
262 self.config.connect_timeout = Some(connect_timeout);
263 self
264 }
265
266 pub fn with_pool_idle_timeout(mut self, pool_idle_timeout: Duration) -> Self {
274 self.config.pool_idle_timeout = Some(pool_idle_timeout);
275 self
276 }
277
278 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
286 self.config.headers = Some(headers);
287 self
288 }
289
290 pub fn with_common_interceptor(self) -> Self {
298 let service = self.config.service.clone();
299 self.with_interceptor(Box::new(CommonInterceptor::new(&service)))
300 }
301
302 pub fn with_pool_max_idle_per_host(mut self, pool_max_idle_per_host: usize) -> Self {
310 self.config.pool_max_idle_per_host = pool_max_idle_per_host;
311 self
312 }
313
314 pub fn with_dns_overrides(mut self, dns_overrides: HashMap<String, Vec<SocketAddr>>) -> Self {
322 self.config.dns_overrides = Some(dns_overrides);
323 self
324 }
325
326 pub fn build(mut self) -> Result<Client> {
334 let mut builder = ReqwestClient::builder()
335 .user_agent(format!("tibba-request/{VERSION}"))
336 .referer(false);
337 if let Some(timeout) = self.config.timeout {
338 builder = builder.timeout(timeout);
339 }
340 if let Some(headers) = self.config.headers.take() {
341 builder = builder.default_headers(headers.clone());
342 }
343 if let Some(read_timeout) = self.config.read_timeout {
344 builder = builder.read_timeout(read_timeout);
345 }
346 if let Some(connect_timeout) = self.config.connect_timeout {
347 builder = builder.connect_timeout(connect_timeout);
348 }
349 if let Some(pool_idle_timeout) = self.config.pool_idle_timeout {
350 builder = builder.pool_idle_timeout(pool_idle_timeout);
351 }
352 if self.config.pool_max_idle_per_host > 0 {
353 builder = builder.pool_max_idle_per_host(self.config.pool_max_idle_per_host);
354 }
355 if let Some(dns_overrides) = self.config.dns_overrides.take() {
356 for (host, addrs) in dns_overrides {
357 builder = builder.resolve_to_addrs(&host, &addrs);
358 }
359 }
360
361 builder = builder.tls_info(true);
362
363 let client = builder.build().map_err(|e| Error::Build {
364 service: self.config.service.clone(),
365 source: e,
366 })?;
367 Ok(Client {
368 client,
369 config: self.config,
370 processing: AtomicU32::new(0),
371 })
372 }
373}
374
375pub struct Client {
377 client: ReqwestClient, config: ClientConfig, processing: AtomicU32, }
381
382impl Client {
383 fn get_url(&self, url: &str) -> String {
385 if url.starts_with("http") {
386 url.to_string()
387 } else {
388 self.config.base_url.to_string() + url
389 }
390 }
391 async fn raw<Q, P>(&self, stats: &mut HttpStats, params: Params<'_, Q, P>) -> Result<Bytes>
393 where
394 Q: Serialize + ?Sized,
395 P: Serialize + ?Sized,
396 {
397 let processing = self.processing.fetch_add(1, Ordering::Relaxed) + 1;
398 defer! {
399 self.processing.fetch_sub(1, Ordering::Relaxed);
400 };
401 if let Some(max_processing) = self.config.max_processing
402 && processing > max_processing
403 {
404 return Err(Error::Common {
405 service: self.config.service.clone(),
406 message: "too many requests".to_string(),
407 });
408 }
409
410 let url = self.get_url(params.url);
411 let uri = url.parse::<Uri>().map_err(|e| Error::Uri {
412 service: self.config.service.clone(),
413 source: e,
414 })?;
415 let path = uri.path();
416 stats.path = path.to_string();
417 stats.method = params.method.to_string();
418
419 let mut req = match params.method {
420 Method::POST => self.client.post(url),
421 Method::PUT => self.client.put(url),
422 Method::PATCH => self.client.patch(url),
423 Method::DELETE => self.client.delete(url),
424 _ => self.client.get(url),
425 };
426 if let Some(value) = params.timeout {
427 req = req.timeout(value);
428 }
429
430 if let Some(value) = params.query {
431 req = req.query(value);
432 }
433 if let Some(value) = params.body {
434 req = req.json(value);
435 }
436 if let Some(interceptors) = &self.config.interceptors {
437 for interceptor in interceptors {
438 req = interceptor.request(req).await?;
439 }
440 }
441 let process_done = Stopwatch::new();
443 let res = req.send().await.map_err(|e| Error::Request {
444 service: self.config.service.clone(),
445 path: path.to_string(),
446 source: e,
447 })?;
448
449 stats.processing = process_done.elapsed_ms();
450
451 if let Some(remote_addr) = res.remote_addr() {
452 stats.remote_addr = remote_addr.to_string();
453 }
454
455 let status = res.status().as_u16();
456 let transfer_done = Stopwatch::new();
457 let mut full = res.bytes().await.map_err(|e| Error::Request {
458 service: self.config.service.clone(),
459 path: path.to_string(),
460 source: e,
461 })?;
462 stats.transfer = transfer_done.elapsed_ms();
463 stats.content_length = full.len();
464 stats.status = status;
465
466 if let Some(interceptors) = &self.config.interceptors {
467 if status >= 400 {
468 for interceptor in interceptors {
469 interceptor.fail(status, &full).await?;
470 }
471 }
472
473 for interceptor in interceptors {
474 full = interceptor.response(full).await?;
475 }
476 }
477 Ok(full)
478 }
479
480 async fn do_request<Q, P, T>(
482 &self,
483 stats: &mut HttpStats,
484 params: Params<'_, Q, P>,
485 ) -> Result<T>
486 where
487 Q: Serialize + ?Sized,
488 P: Serialize + ?Sized,
489 T: DeserializeOwned,
490 {
491 let full = self.raw(stats, params).await?;
492
493 let serde_done = Stopwatch::new();
494 let data = serde_json::from_slice(&full).map_err(|e| Error::Serde {
495 service: self.config.service.clone(),
496 source: e,
497 })?;
498 stats.serde = serde_done.elapsed_ms();
499 Ok(data)
500 }
501
502 async fn request<Q, P, T>(&self, params: Params<'_, Q, P>) -> Result<T>
505 where
506 Q: Serialize + ?Sized,
507 P: Serialize + ?Sized,
508 T: DeserializeOwned,
509 {
510 let mut stats = HttpStats {
511 ..Default::default()
512 };
513 let done = Stopwatch::new();
514 let result = self.do_request(&mut stats, params).await;
515 stats.total = done.elapsed_ms();
516 let mut err = None;
517 if let Err(ref e) = result {
518 err = Some(e)
519 }
520 if let Some(interceptors) = &self.config.interceptors {
521 for interceptor in interceptors {
522 interceptor.on_done(&stats, err).await?;
523 }
524 }
525
526 result
527 }
528
529 pub async fn request_raw<Q, P>(&self, params: Params<'_, Q, P>) -> Result<Bytes>
537 where
538 Q: Serialize + ?Sized,
539 P: Serialize + ?Sized,
540 {
541 let mut stats = HttpStats {
542 ..Default::default()
543 };
544 let done = Stopwatch::new();
545 let result = self.raw(&mut stats, params).await;
546 stats.total = done.elapsed_ms();
547 let mut err = None;
548 if let Err(ref e) = result {
549 err = Some(e)
550 }
551 if let Some(interceptors) = &self.config.interceptors {
552 for interceptor in interceptors {
553 interceptor.on_done(&stats, err).await?;
554 }
555 }
556
557 result
558 }
559 pub async fn get<T>(&self, url: &str) -> Result<T>
567 where
568 T: DeserializeOwned,
569 {
570 self.request(Params {
571 timeout: None,
572 method: Method::GET,
573 url,
574 query: EMPTY_QUERY,
575 body: EMPTY_BODY,
576 })
577 .await
578 }
579 pub async fn get_with_query<P, T>(&self, url: &str, query: &P) -> Result<T>
588 where
589 P: Serialize + ?Sized,
590 T: DeserializeOwned,
591 {
592 self.request(Params {
593 timeout: None,
594 method: Method::GET,
595 url,
596 query: Some(query),
597 body: EMPTY_BODY,
598 })
599 .await
600 }
601 pub async fn post<P, T>(&self, url: &str, json: &P) -> Result<T>
610 where
611 P: Serialize + ?Sized,
612 T: DeserializeOwned,
613 {
614 self.request(Params {
615 timeout: None,
616 method: Method::POST,
617 url,
618 query: EMPTY_QUERY,
619 body: Some(json),
620 })
621 .await
622 }
623 pub async fn post_with_query<P, Q, T>(&self, url: &str, json: &P, query: &Q) -> Result<T>
633 where
634 P: Serialize + ?Sized,
635 Q: Serialize + ?Sized,
636 T: DeserializeOwned,
637 {
638 self.request(Params {
639 timeout: None,
640 method: Method::POST,
641 url,
642 query: Some(query),
643 body: Some(json),
644 })
645 .await
646 }
647 pub fn get_processing(&self) -> u32 {
652 self.processing.load(Ordering::Relaxed)
653 }
654}