Skip to main content

reqwest_rate_limit/
lib.rs

1//! Rate limiting helpers and optional wrapper ergonomics for `reqwest`.
2//!
3//! This crate keeps `reqwest` at the center while adding helpers to apply rate limits
4//! and, optionally, a small wrapper client with middleware hooks.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use governor::Quota;
10//! use std::num::NonZeroU32;
11//! use std::sync::Arc;
12//!
13//! let rate_limiter = Arc::new(governor::RateLimiter::direct(Quota::per_hour(
14//!     NonZeroU32::new(5_000).unwrap(),
15//! )));
16//!
17//! let client = reqwest_rate_limit::Client::builder()
18//!     .user_agent("reqwest-rate-limit-docs")
19//!     .configure(|b| b.timeout(std::time::Duration::from_secs(30)))
20//!     .build()
21//!     .unwrap();
22//!
23//! let _future = client
24//!     .get("https://api.example.com/v1/health")
25//!     .with_rate_limiter(rate_limiter)
26//!     .send();
27//! ```
28//!
29//! # Why `configure`?
30//!
31//! `ClientBuilder::configure` gives you access to the full surface of
32//! `reqwest::ClientBuilder` without this crate having to mirror every method.
33//! That means you can use all of reqwest's options and still keep the wrapper
34//! ergonomics and middleware hooks.
35//!
36//! ```no_run
37//! # use governor::Quota;
38//! # use std::num::NonZeroU32;
39//! # use std::sync::Arc;
40//! # let rate_limiter = Arc::new(governor::RateLimiter::direct(Quota::per_hour(
41//! #     NonZeroU32::new(5_000).unwrap(),
42//! # )));
43//! let client = reqwest_rate_limit::Client::builder()
44//!     .configure(|b| {
45//!         b.timeout(std::time::Duration::from_secs(10))
46//!          .pool_max_idle_per_host(8)
47//!          .https_only(true)
48//!     })
49//!     .rate_limiter(rate_limiter)
50//!     .build()
51//!     .unwrap();
52//! ```
53//!
54//! If you do not want the wrapper, use `send_with_rate_limiter` with a plain
55//! `reqwest::Client` instead.
56//!
57//! # ResponseMiddleware
58//!
59//! Implement `ResponseMiddleware` to inspect responses and apply rate-limit rules.
60//! The `github_rest_api.rs` example shows how to translate
61//! `retry-after` headers into concrete waits and backoff behavior.
62//!
63//! # Features
64//!
65//! This crate forwards optional `reqwest` features:
66//! `json`, `form`, `query`, and `multipart`.
67pub mod reqwest_wrapper;
68
69/// Re-exported for convenience when constructing rate limiters.
70pub use governor;
71/// Wrapper client that layers rate limiting and middleware hooks over `reqwest`.
72pub use reqwest_wrapper::{Client, ClientBuilder, RequestBuilder};
73
74/// Intercept a response to apply rate-limit aware behavior.
75pub trait ResponseMiddleware {
76    type Error;
77
78    /// Inspect or transform the `reqwest` response.
79    ///
80    /// # Examples
81    ///
82    /// ```no_run
83    /// struct Passthrough;
84    ///
85    /// impl reqwest_rate_limit::ResponseMiddleware for Passthrough {
86    ///     type Error = reqwest::Error;
87    ///
88    ///     fn on_response(
89    ///         &self,
90    ///         response: reqwest::Result<reqwest::Response>,
91    ///     ) -> Result<reqwest::Response, Self::Error> {
92    ///         response
93    ///     }
94    /// }
95    /// ```
96    fn on_response(
97        &self,
98        response: reqwest::Result<reqwest::Response>,
99    ) -> Result<reqwest::Response, Self::Error>;
100}
101
102/// Default middleware that returns the response unchanged.
103#[derive(Clone, Default)]
104pub struct NoopResponseMiddleware;
105
106impl ResponseMiddleware for NoopResponseMiddleware {
107    type Error = reqwest::Error;
108
109    fn on_response(
110        &self,
111        response: reqwest::Result<reqwest::Response>,
112    ) -> Result<reqwest::Response, Self::Error> {
113        response
114    }
115}
116
117/// Send a request after waiting for the rate limiter to allow it.
118///
119/// # Examples
120///
121/// ```no_run
122/// use governor::{Quota, RateLimiter};
123/// use std::num::NonZeroU32;
124///
125/// # async fn example() -> reqwest::Result<()> {
126/// let client = reqwest::Client::new();
127/// let request = client.get("https://api.example.com/health");
128/// let limiter = RateLimiter::direct(Quota::per_second(NonZeroU32::new(5).unwrap()));
129/// let _response = reqwest_rate_limit::send_with_rate_limiter(request, &limiter).await?;
130/// # Ok(())
131/// # }
132/// ```
133pub async fn send_with_rate_limiter(
134    request: reqwest::RequestBuilder,
135    rate_limiter: &governor::DefaultDirectRateLimiter,
136) -> reqwest::Result<reqwest::Response> {
137    rate_limiter.until_ready().await;
138    request.send().await
139}
140
141/// Send a request through a response middleware after rate limiting.
142///
143/// # Examples
144///
145/// ```no_run
146/// use governor::{Quota, RateLimiter};
147/// use std::num::NonZeroU32;
148///
149/// struct Passthrough;
150///
151/// impl reqwest_rate_limit::ResponseMiddleware for Passthrough {
152///     type Error = reqwest::Error;
153///
154///     fn on_response(
155///         &self,
156///         response: reqwest::Result<reqwest::Response>,
157///     ) -> Result<reqwest::Response, Self::Error> {
158///         response
159///     }
160/// }
161///
162/// # async fn example() -> Result<(), reqwest::Error> {
163/// let client = reqwest::Client::new();
164/// let request = client.get("https://api.example.com/health");
165/// let limiter = RateLimiter::direct(Quota::per_second(NonZeroU32::new(5).unwrap()));
166/// let middleware = Passthrough;
167/// let _response =
168///     reqwest_rate_limit::send_with_rate_limiter_and_middleware(request, &limiter, &middleware)
169///         .await?;
170/// # Ok(())
171/// # }
172/// ```
173pub async fn send_with_rate_limiter_and_middleware<MW>(
174    request: reqwest::RequestBuilder,
175    rate_limiter: &governor::DefaultDirectRateLimiter,
176    middleware: &MW,
177) -> Result<reqwest::Response, MW::Error>
178where
179    MW: ResponseMiddleware,
180{
181    let response = send_with_rate_limiter(request, rate_limiter).await;
182    middleware.on_response(response)
183}