1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
//! `reqwest-rotate` bundles the three things almost every scraper or API
//! client needs on top of [`reqwest`]: rotating across a pool of proxies,
//! rate-limiting requests per host, and retrying transient failures with
//! backoff that honours `Retry-After`.
//!
//! It is deliberately small: one client, one builder, no middleware traits.
//! Proxies are optional. Without them [`RotatingClient`] is just a
//! rate-limited, retrying wrapper around `reqwest` with sane timeouts.
//!
//! # Example
//!
//! ```no_run
//! use reqwest_rotate::RotatingClient;
//! use std::time::Duration;
//!
//! # async fn run() -> Result<(), reqwest_rotate::Error> {
//! let client = RotatingClient::builder()
//! .proxies([
//! "http://user:pass@proxy1.example:8000",
//! "http://user:pass@proxy2.example:8000",
//! ])
//! .rate_limit(Duration::from_millis(500))
//! .retries(4)
//! .backoff(Duration::from_millis(200), Duration::from_secs(30))
//! .proxy_cooldown(Duration::from_secs(60))
//! .timeout(Duration::from_secs(20))
//! .user_agent("my-scraper/0.1")
//! .build()?;
//!
//! let response = client.get("https://example.com/api").await?;
//! println!("status: {}", response.status());
//! # Ok(())
//! # }
//! ```
//!
//! # What gets retried
//!
//! - Responses with status 408, 429 or 503, for every request; other 5xx
//! (except 501 and 505) only for idempotent requests, since a `POST` the
//! server processed and then failed to answer would be duplicated.
//! Backoff is full-jitter exponential; a `Retry-After` header, when
//! present, replaces the computed delay. If the server asks for a wait
//! longer than `max_retry_after` (30 s by default), the response is
//! returned right away instead of retrying early against its wishes.
//! - A `407` answered by a configured proxy, for every request including
//! `POST`: the proxy did not forward anything, so nothing can be
//! duplicated. The proxy is put in cooldown and the next attempt goes
//! through another one; a `Retry-After` on that `407` is deliberately
//! not honoured, since the wait belongs to the failed proxy, not to the
//! server.
//! - Transport errors that prove the request never reached the server:
//! connect failures (including connect timeouts), requests cancelled
//! before dispatch, HTTP/2 `REFUSED_STREAM`. Retried for every request.
//! - Other transport errors: a total-request timeout, a connection closed
//! or reset before the response arrived (the classic keep-alive race of
//! long-running scrapers), an HTTP/2 `GOAWAY` or stream reset. Retried
//! only for idempotent methods (`GET`, `HEAD`, `OPTIONS`, `PUT`,
//! `DELETE`, `TRACE`).
//!
//! After the last attempt the response is returned as-is, whatever its
//! status, and a transport error is returned as [`Error::Reqwest`]. Its
//! message is the short `request failed`; the cause is the error's
//! [`source`](std::error::Error::source), which `anyhow`'s `{:#}` and most
//! reporters print for you. Each attempt is bounded by the timeouts below,
//! not the whole call; wrap the call in [`tokio::time::timeout`] for a
//! hard overall budget.
//!
//! # Proxies
//!
//! Proxies are used round-robin. A proxy that fails at the transport level
//! (connect failure, timeout, dropped or reset connection) or answers
//! `407 Proxy Authentication Required` is put on cooldown and skipped until
//! the cooldown expires, or until it answers a request again, whichever
//! comes first. A per-attempt timeout counts as the proxy's failure, since
//! the client cannot tell a stalled proxy from a stalled origin; blaming
//! it is cheap: the mark clears the first time the proxy answers again.
//! While another proxy is out of cooldown, the retry goes through it right
//! away; once no other proxy is available, retries are paced by the
//! backoff. Any other status is the origin's answer, and the proxy keeps
//! its place in the rotation.
//!
//! Only proxies you configure are used: the `HTTP_PROXY`, `HTTPS_PROXY`
//! and `ALL_PROXY` environment variables are ignored. `http://` and
//! `https://` proxies work out of the box; `socks5://` and friends need
//! the `socks` cargo feature (without it they are rejected by `build()`
//! instead of silently misbehaving). A bare `host:port` is accepted and
//! treated as `http://host:port`. Proxy credentials are hidden from
//! `Debug` output, error messages, and `tracing` events.
//!
//! # Timeouts
//!
//! A bare `reqwest::Client` has none by default. This one has both: 30 s
//! for the whole attempt, 10 s to connect, each configurable on the
//! builder. They bound one attempt, not the whole call.
//!
//! One trap for tests using `#[tokio::test(start_paused = true)]`: tokio's
//! auto-advancing clock fires the request timeout the moment a task blocks
//! on real socket I/O. Use real time against a local server.
//!
//! # Sharing
//!
//! `RotatingClient` is cheap to clone: clones share the connection pools,
//! the proxy cooldown state, and the rate limiter.
/// Longest duration this crate's clamped knobs honour, and the ceiling
/// used when a configured duration would overflow `Instant` arithmetic.
/// Anything above it (only absurd values such as `Duration::MAX`) is
/// treated as this, so no arithmetic on a configured duration can
/// quietly turn a limit into no limit at all.
pub const MAX_DURATION: Duration =
from_secs;
pub use ;
pub use Error;
pub use ProxyList;
/// `tracing::debug!` with the `tracing` feature on, nothing without it, so
/// call sites need no `#[cfg]` of their own. Helpers that exist only to
/// feed these call sites carry `allow(dead_code)` for feature-off builds.
pub use trace_log;