tibba_request/
request.rs

1// Copyright 2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Import necessary dependencies
16use 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
37// Default empty query and body parameters
38static EMPTY_QUERY: Option<&[(&str, &str)]> = None;
39static EMPTY_BODY: Option<&[(&str, &str)]> = None;
40
41/// Request parameters structure
42/// Generic over query (Q) and body (P) types that must be serializable
43#[derive(Clone, Debug, Default)]
44pub struct Params<'a, Q, P>
45where
46    Q: Serialize + ?Sized,
47    P: Serialize + ?Sized,
48{
49    // http method
50    pub method: Method,
51    // request timeout
52    pub timeout: Option<Duration>,
53    // query parameters
54    pub query: Option<&'a Q>,
55    // request body
56    pub body: Option<&'a P>,
57    // request url
58    pub url: &'a str,
59}
60
61/// Statistics for HTTP requests
62#[derive(Default, Clone, Debug)]
63pub struct HttpStats {
64    pub method: String,         // HTTP method used
65    pub path: String,           // Request path
66    pub remote_addr: String,    // Remote address
67    pub status: u16,            // Response status code
68    pub content_length: usize,  // Response content length
69    pub processing: u32,        // Processing time
70    pub transfer: u32,          // Transfer time
71    pub serde: u32,             // Serialization/deserialization time
72    pub total: u32,             // Total request time
73    pub tls_version: String,    // TLS version used
74    pub tls_not_before: String, // TLS certificate validity start
75    pub tls_not_after: String,  // TLS certificate validity end
76}
77
78/// HTTP interceptor trait for request/response modification and monitoring
79#[async_trait]
80pub trait HttpInterceptor: Send + Sync {
81    // Handle failed requests (status >= 400)
82    async fn fail(&self, _status: u16, _data: &Bytes) -> Result<()> {
83        Ok(())
84    }
85    // Modify outgoing requests
86    async fn request(&self, req: RequestBuilder) -> Result<RequestBuilder> {
87        Ok(req)
88    }
89    // Modify incoming responses
90    async fn response(&self, data: Bytes) -> Result<Bytes> {
91        Ok(data)
92    }
93    // Handle request completion
94    async fn on_done(&self, _stats: &HttpStats, _err: Option<&Error>) -> Result<()> {
95        Ok(())
96    }
97}
98
99/// Common error handling for HTTP responses
100pub 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
114/// Default interceptor implementation with logging
115pub 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
157/// HTTP client configuration
158struct ClientConfig {
159    service: String,                     // Service name
160    base_url: String,                    // Base URL for requests
161    read_timeout: Option<Duration>,      // Read timeout
162    timeout: Option<Duration>,           // Overall timeout
163    connect_timeout: Option<Duration>,   // Connection timeout
164    pool_idle_timeout: Option<Duration>, // Connection pool idle timeout
165    pool_max_idle_per_host: usize,       // Max idle connections per host
166    max_processing: Option<u32>,         // Max concurrent requests
167    headers: Option<HeaderMap>,          // Default headers
168    dns_overrides: Option<HashMap<String, Vec<SocketAddr>>>,
169    interceptors: Option<Vec<Box<dyn HttpInterceptor>>>, // Request interceptors
170}
171
172/// Builder for HTTP client configuration
173pub struct ClientBuilder {
174    config: ClientConfig,
175}
176
177impl ClientBuilder {
178    /// Create a new client builder
179    ///
180    /// # Arguments
181    /// * `service` - Service name
182    ///
183    /// # Returns
184    /// * `ClientBuilder` - A new client builder
185    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    /// Set the base URL for requests
204    ///
205    /// # Arguments
206    /// * `base_url` - Base URL for requests
207    ///
208    /// # Returns
209    /// * `ClientBuilder` - A new client builder
210    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    /// Set the interceptor for requests
216    ///
217    /// # Arguments
218    /// * `interceptor` - Interceptor for requests
219    ///
220    /// # Returns
221    /// * `ClientBuilder` - A new client builder
222    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    /// Set the timeout for requests
231    ///
232    /// # Arguments
233    /// * `timeout` - Timeout for requests
234    ///
235    /// # Returns
236    /// * `ClientBuilder` - A new client builder
237    pub fn with_timeout(mut self, timeout: Duration) -> Self {
238        self.config.timeout = Some(timeout);
239        self
240    }
241
242    /// Set the read timeout for requests
243    ///
244    /// # Arguments
245    /// * `read_timeout` - Read timeout for requests
246    ///
247    /// # Returns
248    /// * `ClientBuilder` - A new client builder
249    pub fn with_read_timeout(mut self, read_timeout: Duration) -> Self {
250        self.config.read_timeout = Some(read_timeout);
251        self
252    }
253
254    /// Set the connect timeout for requests
255    ///
256    /// # Arguments
257    /// * `connect_timeout` - Connect timeout for requests
258    ///
259    /// # Returns
260    /// * `ClientBuilder` - A new client builder
261    pub fn with_connect_timeout(mut self, connect_timeout: Duration) -> Self {
262        self.config.connect_timeout = Some(connect_timeout);
263        self
264    }
265
266    /// Set the pool idle timeout for requests
267    ///
268    /// # Arguments
269    /// * `pool_idle_timeout` - Pool idle timeout for requests
270    ///
271    /// # Returns
272    /// * `ClientBuilder` - A new client builder
273    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    /// Set the headers for requests
279    ///
280    /// # Arguments
281    /// * `headers` - Headers for requests
282    ///
283    /// # Returns
284    /// * `ClientBuilder` - A new client builder
285    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
286        self.config.headers = Some(headers);
287        self
288    }
289
290    /// Set the common interceptor for requests
291    ///
292    /// # Arguments
293    /// * `self` - Client builder
294    ///
295    /// # Returns
296    /// * `ClientBuilder` - A new client builder
297    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    /// Set the pool max idle per host for requests
303    ///
304    /// # Arguments
305    /// * `pool_max_idle_per_host` - Pool max idle per host for requests
306    ///
307    /// # Returns
308    /// * `ClientBuilder` - A new client builder
309    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    /// Set the DNS overrides for requests
315    ///
316    /// # Arguments
317    /// * `dns_overrides` - DNS overrides for requests
318    ///
319    /// # Returns
320    /// * `ClientBuilder` - A new client builder
321    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    /// Build the client
327    ///
328    /// # Arguments
329    /// * `self` - Client builder
330    ///
331    /// # Returns
332    /// * `Result<Client>` - A new client
333    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
375/// HTTP client implementation
376pub struct Client {
377    client: ReqwestClient, // Underlying reqwest client
378    config: ClientConfig,  // Client configuration
379    processing: AtomicU32, // Current processing count
380}
381
382impl Client {
383    /// Constructs full URL from base URL and path
384    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    /// Makes raw HTTP request and returns bytes
392    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        // TODO dns tcp tls process
442        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    /// Makes HTTP request and deserializes response
481    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    // Public API methods for different HTTP methods
503    // GET, POST, etc. with various parameter combinations
504    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    /// Makes raw HTTP request and returns bytes
530    ///
531    /// # Arguments
532    /// * `params` - Request parameters
533    ///
534    /// # Returns
535    /// * `Result<Bytes>` - Raw response bytes
536    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    /// Makes GET request and deserializes response
560    ///
561    /// # Arguments
562    /// * `url` - Request URL
563    ///
564    /// # Returns
565    /// * `Result<T>` - Deserialized response
566    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    /// Makes GET request with query parameters and deserializes response
580    ///
581    /// # Arguments
582    /// * `url` - Request URL
583    /// * `query` - Query parameters
584    ///
585    /// # Returns
586    /// * `Result<T>` - Deserialized response
587    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    /// Makes POST request with JSON body and deserializes response
602    ///
603    /// # Arguments
604    /// * `url` - Request URL
605    /// * `json` - JSON body
606    ///
607    /// # Returns
608    /// * `Result<T>` - Deserialized response
609    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    /// Makes POST request with JSON body and query parameters and deserializes response
624    ///
625    /// # Arguments
626    /// * `url` - Request URL
627    /// * `json` - JSON body
628    /// * `query` - Query parameters
629    ///
630    /// # Returns
631    /// * `Result<T>` - Deserialized response
632    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    /// Gets the current processing count
648    ///
649    /// # Returns
650    /// * `u32` - Current processing count
651    pub fn get_processing(&self) -> u32 {
652        self.processing.load(Ordering::Relaxed)
653    }
654}