#![forbid(unsafe_code, future_incompatible)]
#![deny(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
nonstandard_style,
unused_qualifications,
unused_import_braces,
unused_extern_crates,
trivial_casts,
trivial_numeric_casts
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use governor::{
clock::{Clock, DefaultClock},
state::keyed::DefaultKeyedStateStore,
Quota, RateLimiter,
};
use http_types::{headers, Response, StatusCode};
use lazy_static::lazy_static;
use std::{convert::TryInto, error::Error, num::NonZeroU32, sync::Arc, time::Duration};
use surf::{middleware::Next, Client, Request, Result};
lazy_static! {
static ref CLOCK: DefaultClock = DefaultClock::default();
}
#[derive(Debug, Clone)]
pub struct GovernorMiddleware {
limiter: Arc<RateLimiter<String, DefaultKeyedStateStore<String>, DefaultClock>>,
}
impl GovernorMiddleware {
#[must_use]
pub fn with_period(duration: Duration) -> Option<Self> {
Some(Self {
limiter: Arc::new(RateLimiter::<String, _, _>::keyed(Quota::with_period(
duration,
)?)),
})
}
pub fn per_second<T>(times: T) -> Result<Self>
where
T: TryInto<NonZeroU32>,
T::Error: Error + Send + Sync + 'static,
{
Ok(Self {
limiter: Arc::new(RateLimiter::<String, _, _>::keyed(Quota::per_second(
times.try_into()?,
))),
})
}
pub fn per_minute<T>(times: T) -> Result<Self>
where
T: TryInto<NonZeroU32>,
T::Error: Error + Send + Sync + 'static,
{
Ok(Self {
limiter: Arc::new(RateLimiter::<String, _, _>::keyed(Quota::per_minute(
times.try_into()?,
))),
})
}
pub fn per_hour<T>(times: T) -> Result<Self>
where
T: TryInto<NonZeroU32>,
T::Error: Error + Send + Sync + 'static,
{
Ok(Self {
limiter: Arc::new(RateLimiter::<String, _, _>::keyed(Quota::per_hour(
times.try_into()?,
))),
})
}
}
#[surf::utils::async_trait]
impl surf::middleware::Middleware for GovernorMiddleware {
async fn handle(
&self,
req: Request,
client: Client,
next: Next<'_>,
) -> std::result::Result<surf::Response, http_types::Error> {
match self
.limiter
.check_key(&req.url().host_str().unwrap().to_string())
{
Ok(_) => Ok(next.run(req, client).await?),
Err(negative) => {
let wait_time = negative.wait_time_from(CLOCK.now());
let mut res = Response::new(StatusCode::TooManyRequests);
res.insert_header(headers::RETRY_AFTER, wait_time.as_secs().to_string());
Ok(res.try_into()?)
}
}
}
}
#[cfg(test)]
mod tests {
use crate::GovernorMiddleware;
use surf::{http::Method, Client, Request};
use url::Url;
use wiremock::{matchers::method, Mock, MockServer, ResponseTemplate};
#[async_std::test]
async fn limits_requests() -> surf::Result<()> {
let mock_server = MockServer::start().await;
let m = Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(200).set_body_string("Hello!".to_string()))
.expect(1);
let _mock_guard = mock_server.register_as_scoped(m).await;
let url = format!("{}/", &mock_server.uri());
let req = Request::new(Method::Get, Url::parse(&url).unwrap());
let client = Client::new().with(GovernorMiddleware::per_second(1)?);
let good_res = client.send(req.clone()).await?;
assert_eq!(good_res.status(), 200);
let wait_res = client.send(req).await?;
assert_eq!(wait_res.status(), 429);
Ok(())
}
}