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}