← 中文资源

简体中文版本

Reqwest 重试中间件退避

为 reqwest 客户端叠加重试中间件:指数退避封顶、瞬态响应分类驱动重试,手动循环演示 429 时遵循 Retry-After 头。

阅读英文原文

示例说明

reqwest-retry 把重试实现为 tower 中间件:策略对象声明退避参数——基数二、初始两百毫秒、总延迟封顶十秒、最多三次;响应分类器判定哪些失败值得重试,幂等方法默认在内。中间件形态让重试对调用代码完全透明,同一客户端的全体请求共享策略。

技术要点

  • 策略对象声明退避与次数上限
  • 响应分类器圈定可重试失败
  • Retry-After 优先于本地退避

代码解读

第一段构建带超时的底层客户端,链上 RetryTransientMiddleware 后正常调用即可享受重试;error_for_status 把 4xx/5xx 转成错误参与判定。第二段的手动循环演示精细控制:429 响应读取 retry-after 头换算等待秒数,无头则默认两秒,超过三次放弃并报错。

使用建议

非幂等请求要显式排除或配幂等键。重试日志带尝试序号便于排障。持续 429 说明配额规划问题而非网络抖动。

代码示例

use std::time::Duration;

use reqwest::Client;
use reqwest_middleware::ClientBuilder;
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware, classify_response_for_retry};
use retry_policies::RetryPolicy;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let retry_policy = ExponentialBackoff::builder()
        .base(2)
        .initial(Duration::from_millis(200))
        .max_total_delay(Duration::from_secs(10))
        .max_retries(3)
        .build();

    let client = ClientBuilder::new(Client::builder().timeout(Duration::from_secs(8)).build()?)
        .with(RetryTransientMiddleware::new_with_policy(retry_policy))
        .build();

    let body: serde_json::Value = client
        .get("https://api.example.com/catalog")
        .query(&[("page", "2")])
        .header("x-client", "sync/1.0")
        .send()
        .await?
        .error_for_status()?
        .json()
        .await?;

    println!("items: {}", body.get("items").and_then(|v| v.as_array()).map_or(0, |a| a.len()));

    // 手动重试演示:仅幂等方法、遵循 Retry-After
    let raw = Client::new();
    let mut attempt = 0;
    loop {
        attempt += 1;
        let resp = raw.get("https://api.example.com/rates").send().await?;

        match classify_response_for_retry(&resp) {
            Some(_) if resp.status().as_u16() == 429 => {
                let wait = resp
                    .headers()
                    .get("retry-after")
                    .and_then(|v| v.to_str().ok())
                    .and_then(|s| s.parse::<u64>().ok())
                    .unwrap_or(2);
                if attempt > 3 {
                    return Err("rate limited after retries".into());
                }
                tokio::time::sleep(Duration::from_secs(wait)).await;
            }
            _ => {
                let text = resp.text().await?;
                println!("attempt {attempt} ok, bytes: {}", text.len());
                return Ok(());
            }
        }
    }
}