Rust 用 reqwest 封装健壮 HTTP 客户端:退避重试、超时与中间件

test12026-10-2719 次阅读

为什么不直接用裸请求

业务里直接 reqwest::get 没有超时、没有重试、出错信息也散。把客户端抽成带默认超时、指数退避重试、统一错误类型的结构体,调用方只需关心业务参数。

定义错误与客户端

use std::time::Duration;
use reqwest::Client;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum HttpError {
    #[error("request failed: {0}")]
    Req(#[from] reqwest::Error),
    #[error("max retries exceeded")]
    RetryExhausted,
}

pub struct ApiClient {
    inner: Client,
    retries: u32,
}

impl ApiClient {
    pub fn new(retries: u32) -> Self {
        let inner = Client::builder()
            .timeout(Duration::from_secs(5))
            .build()
            .expect("client build");
        Self { inner, retries }
    }

    pub async fn get_json<T: serde::de::DeserializeOwned>(
        &self,
        url: &str,
    ) -> Result<T, HttpError> {
        let mut attempt: u32 = 0;
        loop {
            match self.inner.get(url).send().await {
                Ok(resp) => return Ok(resp.json::<T>().await?),
                Err(e) if attempt < self.retries => {
                    attempt += 1;
                    let backoff = Duration::from_millis(100 * 2u64.pow(attempt));
                    tokio::time::sleep(backoff).await;
                }
                Err(_) => return Err(HttpError::RetryExhausted),
            }
        }
    }
}

用法

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = ApiClient::new(3);
    let post: serde_json::Value = client
        .get_json("https://api.example.com/post/1")
        .await?;
    println!("{:?}", post);
    Ok(())
}

工程注意点

  • 超时一定要设,否则一个慢响应能卡死整个任务。
  • 重试只针对幂等请求(GET、部分 PUT),写操作重试要先确认幂等。
  • 退避用指数增长并加随机抖动,避免所有失败请求同时重试。
  • 错误类型用 thiserror 收敛,调用方用 ? 一路上抛,不用到处 match。
T

test1

文章作者

为什么不直接用裸请求 业务里直接 reqwest::get 没有超时、没有重试、出错信息也散。把客户端抽成带默认超时、...

分类
技术
发布时间
2026-10-27
字数
约 1699 字
阅读
19 次