Skip to main content

tibba_request/
request.rs

1// Copyright 2026 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
15use 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
37/// 装箱的异步 Future,用于 trait object 场景下的异步方法返回类型。
38pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
39
40/// crate 版本号,注入 User-Agent。
41const VERSION: &str = env!("CARGO_PKG_VERSION");
42
43/// 空查询参数占位符,避免调用方每次传 `None::<&[(&str, &str)]>`。
44const EMPTY_QUERY: Option<&[(&str, &str)]> = None;
45/// 空请求体占位符。
46const EMPTY_BODY: Option<&[(&str, &str)]> = None;
47
48/// HTTP 请求参数,泛型 `Q` 为查询参数类型,`P` 为请求体类型,均须实现 `Serialize`。
49#[derive(Clone, Debug, Default)]
50pub struct Params<'a, Q, P>
51where
52    Q: Serialize + ?Sized,
53    P: Serialize + ?Sized,
54{
55    /// HTTP 方法
56    pub method: Method,
57    /// 单次请求超时,覆盖客户端默认值;`None` 则沿用客户端配置。
58    pub timeout: Option<Duration>,
59    /// URL 查询参数
60    pub query: Option<&'a Q>,
61    /// JSON 请求体
62    pub body: Option<&'a P>,
63    /// 请求 URL(绝对地址或相对于 base_url 的路径)
64    pub url: &'a str,
65}
66
67/// 单次 HTTP 请求的性能统计,各时间字段单位为毫秒。
68#[derive(Default, Clone, Debug)]
69pub struct HttpStats {
70    /// HTTP 方法
71    pub method: String,
72    /// 请求路径
73    pub path: String,
74    /// 服务端远端地址
75    pub remote_addr: String,
76    /// 响应状态码
77    pub status: u16,
78    /// 响应体字节数
79    pub content_length: usize,
80    /// 从发出请求到收到响应头的耗时(毫秒)
81    pub processing: u32,
82    /// 读取完整响应体的耗时(毫秒)
83    pub transfer: u32,
84    /// JSON 反序列化耗时(毫秒)
85    pub serde: u32,
86    /// 请求全程总耗时(毫秒)
87    pub total: u32,
88    /// TLS 版本
89    pub tls_version: String,
90    /// TLS 证书有效期起始时间
91    pub tls_not_before: String,
92    /// TLS 证书有效期截止时间
93    pub tls_not_after: String,
94}
95
96/// HTTP 请求拦截器 trait,用于在请求发出前后注入自定义逻辑(鉴权、日志、错误处理等)。
97pub trait HttpInterceptor: Send + Sync {
98    /// 响应状态码 ≥400 时调用,可将错误信息转换为业务 `Error`。
99    fn fail(&self, _status: u16, _data: &Bytes) -> BoxFuture<'_, Result<()>> {
100        Box::pin(async { Ok(()) })
101    }
102    /// 发送前修改请求(如注入鉴权头、签名等)。
103    fn request(&self, req: RequestBuilder) -> BoxFuture<'_, Result<RequestBuilder>> {
104        Box::pin(async move { Ok(req) })
105    }
106    /// 收到响应体后进行转换(如解密、解包外层结构等)。
107    fn response(&self, data: Bytes) -> BoxFuture<'_, Result<Bytes>> {
108        Box::pin(async move { Ok(data) })
109    }
110    /// 请求完成后(无论成功或失败)的回调,可用于打印日志或上报指标。
111    fn on_done(&self, _stats: &HttpStats, _err: Option<&Error>) -> BoxFuture<'_, Result<()>> {
112        Box::pin(async { Ok(()) })
113    }
114}
115
116/// 从响应体中提取 `message` 字段,状态码 ≥400 时构造业务错误。
117pub 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
131/// 通用日志拦截器,请求完成后通过 tracing 记录详细统计信息。
132pub struct CommonInterceptor {
133    service: String,
134}
135
136impl CommonInterceptor {
137    /// 以服务名创建通用拦截器实例。
138    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    /// 请求完成后打印服务名、方法、路径、状态码、耗时等结构化日志。
152    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
184/// HTTP 客户端内部配置,由 `ClientBuilder` 填充后转移给 `Client`。
185struct ClientConfig {
186    /// 服务名称,用于日志和错误标识
187    service: String,
188    /// 所有相对路径请求的基础 URL
189    base_url: String,
190    /// 读取响应体的超时时间
191    read_timeout: Option<Duration>,
192    /// 整体请求超时时间(含连接 + 传输)
193    timeout: Option<Duration>,
194    /// TCP 连接超时时间
195    connect_timeout: Option<Duration>,
196    /// 连接池空闲超时时间
197    pool_idle_timeout: Option<Duration>,
198    /// 每个 host 最大空闲连接数,0 表示使用默认值
199    pool_max_idle_per_host: usize,
200    /// 最大并发在途请求数,超出时返回 "too many requests" 错误
201    max_processing: Option<u32>,
202    /// 每个请求都附带的默认请求头
203    headers: Option<HeaderMap>,
204    /// 自定义 DNS 解析映射,用于测试或内网转发
205    dns_overrides: Option<HashMap<String, Vec<SocketAddr>>>,
206    /// 请求拦截器链,按注册顺序依次执行
207    interceptors: Option<Vec<Box<dyn HttpInterceptor>>>,
208}
209
210/// HTTP 客户端构建器,通过链式调用配置后调用 `.build()` 生成 `Client`。
211pub struct ClientBuilder {
212    config: ClientConfig,
213}
214
215impl ClientBuilder {
216    /// 以服务名创建构建器,其余选项均使用默认值。
217    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    /// 设置基础 URL,相对路径请求将拼接在此 URL 之后。
236    #[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    /// 追加一个请求拦截器,拦截器按注册顺序链式执行。
243    #[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    /// 设置整体请求超时时间(含建连和传输)。
253    #[must_use]
254    pub fn with_timeout(mut self, timeout: Duration) -> Self {
255        self.config.timeout = Some(timeout);
256        self
257    }
258
259    /// 设置响应体读取超时时间。
260    #[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    /// 设置 TCP 连接超时时间。
267    #[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    /// 设置连接池空闲连接的回收超时时间。
274    #[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    /// 设置每个请求默认携带的请求头。
281    #[must_use]
282    pub fn with_headers(mut self, headers: HeaderMap) -> Self {
283        self.config.headers = Some(headers);
284        self
285    }
286
287    /// 追加通用日志拦截器(`CommonInterceptor`),自动使用当前服务名。
288    #[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    /// 设置每个 host 的最大空闲连接数。
295    #[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    /// 设置最大并发在途请求数,超出时立即返回错误,防止雪崩。
302    #[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    /// 设置自定义 DNS 解析映射,格式为 `host -> [SocketAddr]`。
309    #[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    /// 根据当前配置构建 `Client` 实例。
316    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        // 启用 TLS 信息采集,供拦截器读取证书有效期等元数据
344        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
357/// HTTP 客户端,封装 reqwest `Client`,提供带拦截器链和并发限制的请求方法。
358pub struct Client {
359    /// 底层 reqwest 客户端
360    client: ReqwestClient,
361    /// 客户端配置(服务名、超时、拦截器等)
362    config: ClientConfig,
363    /// 当前在途请求数,用于并发限制
364    processing: AtomicU32,
365}
366
367impl Client {
368    /// 若 `url` 以 "http" 开头则直接使用,否则拼接 base_url。
369    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    /// 执行 HTTP 请求并返回原始响应字节。
378    /// 负责并发计数、拦截器链调用(request / fail / response)及统计采集。
379    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        // 超出并发限制时立即拒绝
389        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        // 依次调用各拦截器的 request 钩子(如注入鉴权头)
422        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            // 状态码 ≥400 时触发各拦截器的 fail 钩子
451            if status >= 400 {
452                for interceptor in interceptors {
453                    interceptor.fail(status, &full).await?;
454                }
455            }
456            // 依次调用各拦截器的 response 钩子(如解包外层结构)
457            for interceptor in interceptors {
458                full = interceptor.response(full).await?;
459            }
460        }
461        Ok(full)
462    }
463
464    /// 执行请求并将响应体反序列化为指定类型,记录反序列化耗时。
465    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    /// 内部通用请求入口:填充统计信息并在完成后触发 `on_done` 拦截器。
486    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    /// 发送请求并返回原始响应字节,不进行 JSON 反序列化。
506    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    /// 发送 GET 请求并将响应反序列化为 `T`。
525    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    /// 发送带查询参数的 GET 请求并将响应反序列化为 `T`。
540    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    /// 发送带 JSON 请求体的 POST 请求并将响应反序列化为 `T`。
556    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    /// 发送带 JSON 请求体和查询参数的 POST 请求并将响应反序列化为 `T`。
572    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    /// 发送带 JSON 请求体的 PUT 请求并将响应反序列化为 `T`。
589    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    /// 发送带 JSON 请求体的 PATCH 请求并将响应反序列化为 `T`。
605    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    /// 发送 DELETE 请求并将响应反序列化为 `T`。
621    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    /// 获取当前在途请求数。
636    pub fn get_processing(&self) -> u32 {
637        self.processing.load(Ordering::Relaxed)
638    }
639}