1use super::{BuildSnafu, Error, LOG_TARGET, RequestSnafu, SerdeSnafu, UriSnafu};
16use axum::http::Method;
17use axum::http::header::HeaderMap;
18use axum::http::uri::Uri;
19use bytes::Bytes;
20use reqwest::Client as ReqwestClient;
21use reqwest::RequestBuilder;
22use scopeguard::defer;
23use serde::Serialize;
24use serde::de::DeserializeOwned;
25use snafu::ResultExt;
26use std::collections::HashMap;
27use std::future::Future;
28use std::net::SocketAddr;
29use std::pin::Pin;
30use std::sync::atomic::{AtomicU32, Ordering};
31use std::time::Duration;
32use tibba_util::{Stopwatch, json_get};
33use tracing::info;
34
35type Result<T> = std::result::Result<T, Error>;
36
37pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
39
40const VERSION: &str = env!("CARGO_PKG_VERSION");
42
43const EMPTY_QUERY: Option<&[(&str, &str)]> = None;
45const EMPTY_BODY: Option<&[(&str, &str)]> = None;
47
48#[derive(Clone, Debug, Default)]
50pub struct Params<'a, Q, P>
51where
52 Q: Serialize + ?Sized,
53 P: Serialize + ?Sized,
54{
55 pub method: Method,
57 pub timeout: Option<Duration>,
59 pub query: Option<&'a Q>,
61 pub body: Option<&'a P>,
63 pub url: &'a str,
65}
66
67#[derive(Default, Clone, Debug)]
69pub struct HttpStats {
70 pub method: String,
72 pub path: String,
74 pub remote_addr: String,
76 pub status: u16,
78 pub content_length: usize,
80 pub processing: u32,
82 pub transfer: u32,
84 pub serde: u32,
86 pub total: u32,
88 pub tls_version: String,
90 pub tls_not_before: String,
92 pub tls_not_after: String,
94}
95
96pub trait HttpInterceptor: Send + Sync {
98 fn fail(&self, _status: u16, _data: &Bytes) -> BoxFuture<'_, Result<()>> {
100 Box::pin(async { Ok(()) })
101 }
102 fn request(&self, req: RequestBuilder) -> BoxFuture<'_, Result<RequestBuilder>> {
104 Box::pin(async move { Ok(req) })
105 }
106 fn response(&self, data: Bytes) -> BoxFuture<'_, Result<Bytes>> {
108 Box::pin(async move { Ok(data) })
109 }
110 fn on_done(&self, _stats: &HttpStats, _err: Option<&Error>) -> BoxFuture<'_, Result<()>> {
112 Box::pin(async { Ok(()) })
113 }
114}
115
116pub fn handle_fail(service: &str, status: u16, data: &Bytes) -> Result<()> {
118 if status >= 400 {
119 let mut message = json_get(data, "message");
120 if message.is_empty() {
121 message = "unknown error".to_string();
122 }
123 return Err(Error::Common {
124 service: service.to_string(),
125 message,
126 });
127 }
128 Ok(())
129}
130
131pub struct CommonInterceptor {
133 service: String,
134}
135
136impl CommonInterceptor {
137 pub fn new(service: &str) -> Self {
139 Self {
140 service: service.to_string(),
141 }
142 }
143}
144
145impl HttpInterceptor for CommonInterceptor {
146 fn fail(&self, status: u16, data: &Bytes) -> BoxFuture<'_, Result<()>> {
147 let result = handle_fail(&self.service, status, data);
148 Box::pin(async move { result })
149 }
150
151 fn on_done(&self, stats: &HttpStats, err: Option<&Error>) -> BoxFuture<'_, Result<()>> {
153 let error = err.map(ToString::to_string);
154 let service = self.service.clone();
155 let method = stats.method.clone();
156 let path = stats.path.clone();
157 let status = stats.status;
158 let remote_addr = stats.remote_addr.clone();
159 let content_length = stats.content_length;
160 let processing = stats.processing;
161 let transfer = stats.transfer;
162 let serde = stats.serde;
163 let total = stats.total;
164 Box::pin(async move {
165 info!(
166 target: LOG_TARGET,
167 service,
168 method,
169 path,
170 status,
171 remote_addr,
172 content_length,
173 processing,
174 transfer,
175 serde,
176 total,
177 error,
178 );
179 Ok(())
180 })
181 }
182}
183
184struct ClientConfig {
186 service: String,
188 base_url: String,
190 read_timeout: Option<Duration>,
192 timeout: Option<Duration>,
194 connect_timeout: Option<Duration>,
196 pool_idle_timeout: Option<Duration>,
198 pool_max_idle_per_host: usize,
200 max_processing: Option<u32>,
202 headers: Option<HeaderMap>,
204 dns_overrides: Option<HashMap<String, Vec<SocketAddr>>>,
206 interceptors: Option<Vec<Box<dyn HttpInterceptor>>>,
208}
209
210pub struct ClientBuilder {
212 config: ClientConfig,
213}
214
215impl ClientBuilder {
216 pub fn new(service: &str) -> Self {
218 Self {
219 config: ClientConfig {
220 service: service.to_string(),
221 base_url: String::new(),
222 read_timeout: None,
223 timeout: None,
224 connect_timeout: None,
225 pool_idle_timeout: None,
226 pool_max_idle_per_host: 0,
227 headers: None,
228 interceptors: None,
229 max_processing: None,
230 dns_overrides: None,
231 },
232 }
233 }
234
235 #[must_use]
237 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
238 self.config.base_url = base_url.into();
239 self
240 }
241
242 #[must_use]
244 pub fn with_interceptor(mut self, interceptor: Box<dyn HttpInterceptor>) -> Self {
245 self.config
246 .interceptors
247 .get_or_insert_with(Vec::new)
248 .push(interceptor);
249 self
250 }
251
252 #[must_use]
254 pub fn with_timeout(mut self, timeout: Duration) -> Self {
255 self.config.timeout = Some(timeout);
256 self
257 }
258
259 #[must_use]
261 pub fn with_read_timeout(mut self, read_timeout: Duration) -> Self {
262 self.config.read_timeout = Some(read_timeout);
263 self
264 }
265
266 #[must_use]
268 pub fn with_connect_timeout(mut self, connect_timeout: Duration) -> Self {
269 self.config.connect_timeout = Some(connect_timeout);
270 self
271 }
272
273 #[must_use]
275 pub fn with_pool_idle_timeout(mut self, pool_idle_timeout: Duration) -> Self {
276 self.config.pool_idle_timeout = Some(pool_idle_timeout);
277 self
278 }
279
280 #[must_use]
282 pub fn with_headers(mut self, headers: HeaderMap) -> Self {
283 self.config.headers = Some(headers);
284 self
285 }
286
287 #[must_use]
289 pub fn with_common_interceptor(self) -> Self {
290 let service = self.config.service.clone();
291 self.with_interceptor(Box::new(CommonInterceptor::new(&service)))
292 }
293
294 #[must_use]
296 pub fn with_pool_max_idle_per_host(mut self, pool_max_idle_per_host: usize) -> Self {
297 self.config.pool_max_idle_per_host = pool_max_idle_per_host;
298 self
299 }
300
301 #[must_use]
303 pub fn with_max_processing(mut self, max_processing: u32) -> Self {
304 self.config.max_processing = Some(max_processing);
305 self
306 }
307
308 #[must_use]
310 pub fn with_dns_overrides(mut self, dns_overrides: HashMap<String, Vec<SocketAddr>>) -> Self {
311 self.config.dns_overrides = Some(dns_overrides);
312 self
313 }
314
315 pub fn build(mut self) -> Result<Client> {
317 let mut builder = ReqwestClient::builder()
318 .user_agent(format!("tibba-request/{VERSION}"))
319 .referer(false);
320 if let Some(timeout) = self.config.timeout {
321 builder = builder.timeout(timeout);
322 }
323 if let Some(headers) = self.config.headers.take() {
324 builder = builder.default_headers(headers);
325 }
326 if let Some(read_timeout) = self.config.read_timeout {
327 builder = builder.read_timeout(read_timeout);
328 }
329 if let Some(connect_timeout) = self.config.connect_timeout {
330 builder = builder.connect_timeout(connect_timeout);
331 }
332 if let Some(pool_idle_timeout) = self.config.pool_idle_timeout {
333 builder = builder.pool_idle_timeout(pool_idle_timeout);
334 }
335 if self.config.pool_max_idle_per_host > 0 {
336 builder = builder.pool_max_idle_per_host(self.config.pool_max_idle_per_host);
337 }
338 if let Some(dns_overrides) = self.config.dns_overrides.take() {
339 for (host, addrs) in dns_overrides {
340 builder = builder.resolve_to_addrs(&host, &addrs);
341 }
342 }
343 builder = builder.tls_info(true);
345
346 let client = builder.build().context(BuildSnafu {
347 service: self.config.service.clone(),
348 })?;
349 Ok(Client {
350 client,
351 config: self.config,
352 processing: AtomicU32::new(0),
353 })
354 }
355}
356
357pub struct Client {
359 client: ReqwestClient,
361 config: ClientConfig,
363 processing: AtomicU32,
365}
366
367impl Client {
368 fn get_url(&self, url: &str) -> String {
370 if url.starts_with("http") {
371 url.to_string()
372 } else {
373 self.config.base_url.to_string() + url
374 }
375 }
376
377 async fn raw<Q, P>(&self, stats: &mut HttpStats, params: Params<'_, Q, P>) -> Result<Bytes>
380 where
381 Q: Serialize + ?Sized,
382 P: Serialize + ?Sized,
383 {
384 let processing = self.processing.fetch_add(1, Ordering::Relaxed) + 1;
385 defer! {
386 self.processing.fetch_sub(1, Ordering::Relaxed);
387 };
388 if let Some(max_processing) = self.config.max_processing
390 && processing > max_processing
391 {
392 return Err(Error::Common {
393 service: self.config.service.clone(),
394 message: "too many requests".to_string(),
395 });
396 }
397
398 let url = self.get_url(params.url);
399 let uri = url.parse::<Uri>().context(UriSnafu {
400 service: self.config.service.clone(),
401 })?;
402 stats.path = uri.path().to_string();
403 stats.method = params.method.to_string();
404
405 let mut req = match params.method {
406 Method::POST => self.client.post(url),
407 Method::PUT => self.client.put(url),
408 Method::PATCH => self.client.patch(url),
409 Method::DELETE => self.client.delete(url),
410 _ => self.client.get(url),
411 };
412 if let Some(value) = params.timeout {
413 req = req.timeout(value);
414 }
415 if let Some(value) = params.query {
416 req = req.query(value);
417 }
418 if let Some(value) = params.body {
419 req = req.json(value);
420 }
421 if let Some(interceptors) = &self.config.interceptors {
423 for interceptor in interceptors {
424 req = interceptor.request(req).await?;
425 }
426 }
427 let process_done = Stopwatch::new();
428 let res = req.send().await.context(RequestSnafu {
429 service: self.config.service.clone(),
430 path: stats.path.clone(),
431 })?;
432
433 stats.processing = process_done.elapsed_ms();
434
435 if let Some(remote_addr) = res.remote_addr() {
436 stats.remote_addr = remote_addr.to_string();
437 }
438
439 let status = res.status().as_u16();
440 let transfer_done = Stopwatch::new();
441 let mut full = res.bytes().await.context(RequestSnafu {
442 service: self.config.service.clone(),
443 path: stats.path.clone(),
444 })?;
445 stats.transfer = transfer_done.elapsed_ms();
446 stats.content_length = full.len();
447 stats.status = status;
448
449 if let Some(interceptors) = &self.config.interceptors {
450 if status >= 400 {
452 for interceptor in interceptors {
453 interceptor.fail(status, &full).await?;
454 }
455 }
456 for interceptor in interceptors {
458 full = interceptor.response(full).await?;
459 }
460 }
461 Ok(full)
462 }
463
464 async fn do_request<Q, P, T>(
466 &self,
467 stats: &mut HttpStats,
468 params: Params<'_, Q, P>,
469 ) -> Result<T>
470 where
471 Q: Serialize + ?Sized,
472 P: Serialize + ?Sized,
473 T: DeserializeOwned,
474 {
475 let full = self.raw(stats, params).await?;
476
477 let serde_done = Stopwatch::new();
478 let data = serde_json::from_slice(&full).context(SerdeSnafu {
479 service: self.config.service.clone(),
480 })?;
481 stats.serde = serde_done.elapsed_ms();
482 Ok(data)
483 }
484
485 async fn request<Q, P, T>(&self, params: Params<'_, Q, P>) -> Result<T>
487 where
488 Q: Serialize + ?Sized,
489 P: Serialize + ?Sized,
490 T: DeserializeOwned,
491 {
492 let mut stats = HttpStats::default();
493 let done = Stopwatch::new();
494 let result = self.do_request(&mut stats, params).await;
495 stats.total = done.elapsed_ms();
496 let err = result.as_ref().err();
497 if let Some(interceptors) = &self.config.interceptors {
498 for interceptor in interceptors {
499 interceptor.on_done(&stats, err).await?;
500 }
501 }
502 result
503 }
504
505 pub async fn request_raw<Q, P>(&self, params: Params<'_, Q, P>) -> Result<Bytes>
507 where
508 Q: Serialize + ?Sized,
509 P: Serialize + ?Sized,
510 {
511 let mut stats = HttpStats::default();
512 let done = Stopwatch::new();
513 let result = self.raw(&mut stats, params).await;
514 stats.total = done.elapsed_ms();
515 let err = result.as_ref().err();
516 if let Some(interceptors) = &self.config.interceptors {
517 for interceptor in interceptors {
518 interceptor.on_done(&stats, err).await?;
519 }
520 }
521 result
522 }
523
524 pub async fn get<T>(&self, url: &str) -> Result<T>
526 where
527 T: DeserializeOwned,
528 {
529 self.request(Params {
530 timeout: None,
531 method: Method::GET,
532 url,
533 query: EMPTY_QUERY,
534 body: EMPTY_BODY,
535 })
536 .await
537 }
538
539 pub async fn get_with_query<P, T>(&self, url: &str, query: &P) -> Result<T>
541 where
542 P: Serialize + ?Sized,
543 T: DeserializeOwned,
544 {
545 self.request(Params {
546 timeout: None,
547 method: Method::GET,
548 url,
549 query: Some(query),
550 body: EMPTY_BODY,
551 })
552 .await
553 }
554
555 pub async fn post<P, T>(&self, url: &str, json: &P) -> Result<T>
557 where
558 P: Serialize + ?Sized,
559 T: DeserializeOwned,
560 {
561 self.request(Params {
562 timeout: None,
563 method: Method::POST,
564 url,
565 query: EMPTY_QUERY,
566 body: Some(json),
567 })
568 .await
569 }
570
571 pub async fn post_with_query<P, Q, T>(&self, url: &str, json: &P, query: &Q) -> Result<T>
573 where
574 P: Serialize + ?Sized,
575 Q: Serialize + ?Sized,
576 T: DeserializeOwned,
577 {
578 self.request(Params {
579 timeout: None,
580 method: Method::POST,
581 url,
582 query: Some(query),
583 body: Some(json),
584 })
585 .await
586 }
587
588 pub async fn put<P, T>(&self, url: &str, json: &P) -> Result<T>
590 where
591 P: Serialize + ?Sized,
592 T: DeserializeOwned,
593 {
594 self.request(Params {
595 timeout: None,
596 method: Method::PUT,
597 url,
598 query: EMPTY_QUERY,
599 body: Some(json),
600 })
601 .await
602 }
603
604 pub async fn patch<P, T>(&self, url: &str, json: &P) -> Result<T>
606 where
607 P: Serialize + ?Sized,
608 T: DeserializeOwned,
609 {
610 self.request(Params {
611 timeout: None,
612 method: Method::PATCH,
613 url,
614 query: EMPTY_QUERY,
615 body: Some(json),
616 })
617 .await
618 }
619
620 pub async fn delete<T>(&self, url: &str) -> Result<T>
622 where
623 T: DeserializeOwned,
624 {
625 self.request(Params {
626 timeout: None,
627 method: Method::DELETE,
628 url,
629 query: EMPTY_QUERY,
630 body: EMPTY_BODY,
631 })
632 .await
633 }
634
635 pub fn get_processing(&self) -> u32 {
637 self.processing.load(Ordering::Relaxed)
638 }
639}