use crate::error::Decision;
use crate::governor::Governor;
use crate::limiters::Limiter;
use crate::matchers::RequestContext;
use http::{Request, Response, StatusCode};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tower::Service;
use tower_layer::Layer;
use super::headers::{RateLimitHeaderValues, inject_rate_limit_headers};
type BoxError = Box<dyn std::error::Error + Send + Sync>;
fn into_box_error<E: Into<BoxError>>(e: E) -> BoxError {
e.into()
}
#[derive(Debug, Clone)]
pub struct RateLimitConfig {
pub return_429_on_reject: bool,
pub return_403_on_ban: bool,
pub reject_body: String,
pub ban_body: String,
pub skip_health_checks: bool,
pub health_check_paths: Vec<String>,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
return_429_on_reject: true,
return_403_on_ban: true,
reject_body: crate::i18n::t_simple("rate-limit-exceeded"),
ban_body: crate::i18n::t_simple("access-denied"),
skip_health_checks: true,
health_check_paths: vec![
"/health".to_string(),
"/healthz".to_string(),
"/ready".to_string(),
],
}
}
}
impl RateLimitConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_return_429_on_reject(mut self, value: bool) -> Self {
self.return_429_on_reject = value;
self
}
pub fn with_return_403_on_ban(mut self, value: bool) -> Self {
self.return_403_on_ban = value;
self
}
pub fn with_reject_body(mut self, body: &str) -> Self {
self.reject_body = body.to_string();
self
}
pub fn with_ban_body(mut self, body: &str) -> Self {
self.ban_body = body.to_string();
self
}
pub fn with_skip_health_checks(mut self, skip: bool) -> Self {
self.skip_health_checks = skip;
self
}
pub fn with_health_check_path(mut self, path: &str) -> Self {
self.health_check_paths.push(path.to_string());
self
}
pub fn is_health_check_path(&self, path: &str) -> bool {
self.skip_health_checks && self.health_check_paths.iter().any(|p| p == path)
}
}
pub trait IntoRequestContext<B> {
#[allow(clippy::wrong_self_convention)]
fn into_request_context(&self, request: &Request<B>) -> RequestContext;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RejectInfo {
pub limit: Option<u64>,
pub reset_at: Option<u64>,
pub retry_after: Option<u64>,
}
pub trait RejectResponder<ResBody> {
fn reject_response(&self, info: RejectInfo) -> Response<ResBody>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct IetfHttpResponder;
impl<ResBody> RejectResponder<ResBody> for IetfHttpResponder
where
ResBody: Default,
{
fn reject_response(&self, info: RejectInfo) -> Response<ResBody> {
let mut response = Response::new(ResBody::default());
*response.status_mut() = StatusCode::TOO_MANY_REQUESTS;
if let (Some(limit), Some(reset_at)) = (info.limit, info.reset_at) {
let header_values = RateLimitHeaderValues {
limit,
remaining: 0,
reset_at,
retry_after: info.retry_after,
policy: String::new(),
};
return inject_rate_limit_headers(response, &header_values);
}
response
}
}
#[derive(Debug, Clone, Copy)]
pub struct PeerAddr(pub std::net::SocketAddr);
#[derive(Debug, Clone, Default)]
pub struct DefaultRequestContextConverter {
trusted_proxies: Vec<ipnet::IpNet>,
}
impl DefaultRequestContextConverter {
pub fn with_trusted_proxies(mut self, proxies: Vec<ipnet::IpNet>) -> Self {
self.trusted_proxies = proxies;
self
}
fn peer_trusted(&self, peer: &PeerAddr) -> bool {
!self.trusted_proxies.is_empty()
&& self
.trusted_proxies
.iter()
.any(|net| net.contains(&peer.0.ip()))
}
}
impl<B> IntoRequestContext<B> for DefaultRequestContextConverter {
fn into_request_context(&self, request: &Request<B>) -> RequestContext {
let mut context = RequestContext::new()
.with_path(request.uri().path())
.with_method(request.method().as_str());
if let Some(user_id) = request.headers().get("x-user-id")
&& let Ok(value) = user_id.to_str()
{
context = context.with_header("X-User-Id", value);
}
let peer = request.extensions().get::<PeerAddr>().copied();
let trust_forwarded = peer.as_ref().map(|p| self.peer_trusted(p)).unwrap_or(false);
let mut forwarded_ip: Option<String> = None;
if trust_forwarded {
if let Some(ip) = request.headers().get("x-real-ip")
&& let Ok(value) = ip.to_str()
{
forwarded_ip = Some(value.to_string());
} else if let Some(forwarded) = request.headers().get("x-forwarded-for")
&& let Ok(value) = forwarded.to_str()
&& let Some(first_ip) = value.split(',').next()
{
forwarded_ip = Some(first_ip.trim().to_string());
}
}
if let Some(ip) = forwarded_ip {
context = context.with_client_ip(&ip);
} else if let Some(p) = peer {
context = context.with_client_ip(&p.0.ip().to_string());
}
if let Some(api_key) = request.headers().get("x-api-key")
&& let Ok(value) = api_key.to_str()
{
context = context.with_header("X-API-Key", value);
}
for (name, value) in request.headers().iter() {
if let Ok(value_str) = value.to_str() {
context = context.with_header(name.as_str(), value_str);
}
}
context
}
}
pub struct RateLimitLayer<C = DefaultRequestContextConverter, R = IetfHttpResponder> {
governor: Arc<Governor>,
config: RateLimitConfig,
context_converter: C,
responder: R,
}
impl RateLimitLayer {
pub fn new(governor: Arc<Governor>, config: RateLimitConfig) -> Self {
Self {
governor,
config,
context_converter: DefaultRequestContextConverter::default(),
responder: IetfHttpResponder,
}
}
}
impl<C> RateLimitLayer<C> {
pub fn with_converter(governor: Arc<Governor>, config: RateLimitConfig, converter: C) -> Self {
Self {
governor,
config,
context_converter: converter,
responder: IetfHttpResponder,
}
}
}
impl<C, R> RateLimitLayer<C, R> {
pub fn with_responder<R2>(self, responder: R2) -> RateLimitLayer<C, R2> {
RateLimitLayer {
governor: self.governor,
config: self.config,
context_converter: self.context_converter,
responder,
}
}
}
impl<S, C, R> Layer<S> for RateLimitLayer<C, R>
where
C: Clone,
R: Clone,
{
type Service = RateLimitService<S, C, R>;
fn layer(&self, inner: S) -> Self::Service {
RateLimitService {
governor: self.governor.clone(),
inner,
config: self.config.clone(),
context_converter: self.context_converter.clone(),
responder: self.responder.clone(),
}
}
}
pub struct RateLimitService<S, C = DefaultRequestContextConverter, R = IetfHttpResponder> {
governor: Arc<Governor>,
inner: S,
config: RateLimitConfig,
context_converter: C,
responder: R,
}
impl<S, C, R, ReqBody, ResBody> Service<Request<ReqBody>> for RateLimitService<S, C, R>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
S::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send,
S::Future: Send + 'static,
C: IntoRequestContext<ReqBody> + Send + Sync + Clone + 'static,
R: RejectResponder<ResBody> + Send + Sync + Clone + 'static,
ReqBody: Send + 'static,
ResBody: Default + Send + 'static,
{
type Response = Response<ResBody>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner
.poll_ready(cx)
.map_err(into_box_error)
.map_ok(|_| ())
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let governor = self.governor.clone();
let config = self.config.clone();
let context_converter = self.context_converter.clone();
let responder = self.responder.clone();
let path = req.uri().path().to_string();
if config.is_health_check_path(&path) {
let future = self.inner.call(req);
return Box::pin(async move { future.await.map_err(into_box_error) });
}
let context = context_converter.into_request_context(&req);
let inner = self.inner.clone().call(req);
Box::pin(async move {
match governor.check(&context).await {
Ok(Decision::Allowed(metadata)) => {
let response = inner.await.map_err(into_box_error)?;
let header_values = RateLimitHeaderValues {
limit: metadata.limit,
remaining: metadata.remaining,
reset_at: metadata.reset_at,
retry_after: metadata.retry_after,
policy: metadata.policy,
};
Ok(inject_rate_limit_headers(response, &header_values))
}
Ok(Decision::Rejected(metadata)) => {
if config.return_429_on_reject {
Ok(responder.reject_response(RejectInfo {
limit: Some(metadata.limit),
reset_at: Some(metadata.reset_at),
retry_after: Some(metadata.retry_after),
}))
} else {
inner.await.map_err(into_box_error)
}
}
Ok(Decision::Banned(_)) => {
if config.return_403_on_ban {
let mut response = Response::new(ResBody::default());
*response.status_mut() = StatusCode::FORBIDDEN;
Ok(response)
} else {
inner.await.map_err(into_box_error)
}
}
Err(e) => {
log::error!("Rate limit check failed: {}", e);
inner.await.map_err(into_box_error)
}
}
})
}
}
pub trait RequestKey<B> {
fn request_key<'a>(&self, request: &'a Request<B>) -> Option<std::borrow::Cow<'a, str>>;
}
#[derive(Debug, Clone)]
pub struct HeaderKeyExtractor {
header: &'static str,
max_key_len: usize,
}
impl HeaderKeyExtractor {
pub const DEFAULT_MAX_KEY_LEN: usize = 128;
pub fn new(header: &'static str) -> Self {
Self {
header,
max_key_len: Self::DEFAULT_MAX_KEY_LEN,
}
}
pub fn with_max_key_len(mut self, max_key_len: usize) -> Self {
self.max_key_len = max_key_len;
self
}
}
impl<B> RequestKey<B> for HeaderKeyExtractor {
fn request_key<'a>(&self, request: &'a Request<B>) -> Option<std::borrow::Cow<'a, str>> {
let value = request.headers().get(self.header)?.to_str().ok()?;
if self.max_key_len == 0 || value.len() <= self.max_key_len {
return Some(std::borrow::Cow::Borrowed(value));
}
Some(std::borrow::Cow::Borrowed(&value[..self.max_key_len]))
}
}
pub struct KeyedRateLimitLayer<L, K, R = IetfHttpResponder> {
limiter: Arc<L>,
key_extractor: K,
responder: R,
}
impl<L, K> KeyedRateLimitLayer<L, K> {
pub fn new(limiter: Arc<L>, key_extractor: K) -> Self {
Self {
limiter,
key_extractor,
responder: IetfHttpResponder,
}
}
}
impl<L, K, R> KeyedRateLimitLayer<L, K, R> {
pub fn with_responder(limiter: Arc<L>, key_extractor: K, responder: R) -> Self {
Self {
limiter,
key_extractor,
responder,
}
}
}
impl<S, L, K, R> Layer<S> for KeyedRateLimitLayer<L, K, R>
where
K: Clone,
R: Clone,
{
type Service = KeyedRateLimitService<S, L, K, R>;
fn layer(&self, inner: S) -> Self::Service {
KeyedRateLimitService {
limiter: self.limiter.clone(),
inner,
key_extractor: self.key_extractor.clone(),
responder: self.responder.clone(),
}
}
}
pub struct KeyedRateLimitService<S, L, K, R = IetfHttpResponder> {
limiter: Arc<L>,
inner: S,
key_extractor: K,
responder: R,
}
impl<S, L, K, R, ReqBody, ResBody> Service<Request<ReqBody>> for KeyedRateLimitService<S, L, K, R>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
S::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send,
S::Future: Send + 'static,
L: Limiter + 'static,
K: RequestKey<ReqBody> + Send + Sync + Clone + 'static,
R: RejectResponder<ResBody> + Send + Sync + Clone + 'static,
ReqBody: Send + 'static,
ResBody: Default + Send + 'static,
{
type Response = Response<ResBody>;
type Error = Box<dyn std::error::Error + Send + Sync>;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner
.poll_ready(cx)
.map_err(into_box_error)
.map_ok(|_| ())
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let limiter = self.limiter.clone();
let responder = self.responder.clone();
let key_extractor = self.key_extractor.clone();
let mut inner = self.inner.clone();
Box::pin(async move {
let Some(key) = key_extractor.request_key(&req) else {
return inner.call(req).await.map_err(into_box_error);
};
match limiter.check(&key).await {
Ok(()) => inner.call(req).await.map_err(into_box_error),
Err(_) => Ok(responder.reject_response(RejectInfo::default())),
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Action, ActionConfig, FlowControlConfig, LimiterConfig, Matcher, Rule};
use crate::error::LimiteronError;
use crate::storage::{MemoryBanStorage, MemoryStorage};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[derive(Clone)]
struct MockService;
impl<B> Service<Request<B>> for MockService {
type Response = Response<()>;
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<B>) -> Self::Future {
Box::pin(async { Ok(Response::new(())) })
}
}
async fn make_governor(
config: FlowControlConfig,
l1_enabled: bool,
) -> (Arc<Governor>, Arc<dyn crate::storage::BanStorage>) {
let storage: Arc<dyn crate::storage::Storage> = Arc::new(MemoryStorage::new());
let ban_storage: Arc<dyn crate::storage::BanStorage> = Arc::new(MemoryBanStorage::new());
let bs = ban_storage.clone();
let gov = Governor::builder()
.with_config(config)
.with_storage(storage)
.with_ban_storage(ban_storage)
.with_l1_cache_enabled(l1_enabled)
.build()
.await
.expect("Governor build");
(Arc::new(gov), bs)
}
fn gen_config(capacity: u64, refill_rate: u64) -> FlowControlConfig {
FlowControlConfig {
rules: vec![Rule {
id: "r".into(),
name: "R".into(),
priority: 100,
matchers: vec![Matcher::User {
user_ids: vec!["*".into()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity,
refill_rate,
}],
action: ActionConfig {
on_exceed: Action::Reject,
ban: None,
},
}],
..FlowControlConfig::default()
}
}
fn make_req(path: &str, user: &str) -> Request<()> {
Request::builder()
.uri(path)
.method("GET")
.header("X-User-Id", user)
.body(())
.unwrap()
}
#[test]
fn test_rate_limit_config_default() {
let config = RateLimitConfig::default();
assert!(config.return_429_on_reject);
assert!(config.return_403_on_ban);
assert!(config.skip_health_checks);
assert_eq!(config.health_check_paths.len(), 3);
assert!(config.is_health_check_path("/health"));
assert!(config.is_health_check_path("/healthz"));
assert!(config.is_health_check_path("/ready"));
assert!(!config.is_health_check_path("/api/users"));
}
#[test]
fn test_rate_limit_config_builder() {
let config = RateLimitConfig::new()
.with_return_429_on_reject(false)
.with_return_403_on_ban(false)
.with_reject_body("Custom reject")
.with_ban_body("Custom ban")
.with_skip_health_checks(false)
.with_health_check_path("/ping");
assert!(!config.return_429_on_reject);
assert!(!config.return_403_on_ban);
assert_eq!(config.reject_body, "Custom reject");
assert_eq!(config.ban_body, "Custom ban");
assert!(!config.skip_health_checks);
assert!(!config.is_health_check_path("/health")); assert!(config.health_check_paths.contains(&"/ping".to_string()));
}
#[test]
fn test_default_request_context_converter() {
use http::Request;
let converter = DefaultRequestContextConverter::default();
let request = Request::builder()
.uri("/api/users")
.method("GET")
.header("X-User-Id", "user123")
.header("X-Real-IP", "192.168.1.1")
.header("X-API-Key", "my-api-key")
.body(())
.unwrap();
let context = converter.into_request_context(&request);
assert_eq!(context.path, "/api/users");
assert_eq!(context.method, "GET");
}
#[tokio::test]
async fn test_rate_limit_layer_creation() {
use crate::config::{
Action, ActionConfig, FlowControlConfig, LimiterConfig, Matcher, Rule,
};
use crate::storage::{MemoryBanStorage, MemoryStorage};
use std::sync::Arc;
let config = FlowControlConfig {
rules: vec![Rule {
id: "test_rule".to_string(),
name: "Test Rule".to_string(),
priority: 100,
matchers: vec![Matcher::User {
user_ids: vec!["*".to_string()],
}],
limiters: vec![LimiterConfig::TokenBucket {
capacity: 100,
refill_rate: 10,
}],
action: ActionConfig {
on_exceed: Action::Reject,
ban: None,
},
}],
..FlowControlConfig::default()
};
let storage: Arc<dyn crate::storage::Storage> = Arc::new(MemoryStorage::new());
let ban_storage: Arc<dyn crate::storage::BanStorage> = Arc::new(MemoryBanStorage::new());
let governor = Governor::builder()
.with_config(config)
.with_storage(storage)
.with_ban_storage(ban_storage)
.build()
.await
.expect("Governor build should succeed");
let layer = RateLimitLayer::new(Arc::new(governor), RateLimitConfig::default());
assert!(layer.config.skip_health_checks);
}
#[test]
fn test_into_box_error_with_string() {
let result = into_box_error("custom error".to_string());
assert_eq!(result.to_string(), "custom error");
}
#[test]
fn test_default_converter_x_forwarded_for() {
let c = DefaultRequestContextConverter::default()
.with_trusted_proxies(vec!["10.0.0.0/8".parse().unwrap()]);
let mut req = Request::builder()
.uri("/api")
.method("GET")
.header("X-Forwarded-For", "10.0.0.1")
.body(())
.unwrap();
req.extensions_mut()
.insert(PeerAddr("10.0.0.254:1000".parse().unwrap()));
let ctx = c.into_request_context(&req);
assert_eq!(ctx.client_ip.as_deref(), Some("10.0.0.1"));
}
#[test]
fn test_default_converter_x_forwarded_for_multiple_ips() {
let c = DefaultRequestContextConverter::default()
.with_trusted_proxies(vec!["10.0.0.0/8".parse().unwrap()]);
let mut req = Request::builder()
.uri("/api")
.method("GET")
.header("X-Forwarded-For", "192.168.1.1, 10.0.0.1, 172.16.0.1")
.body(())
.unwrap();
req.extensions_mut()
.insert(PeerAddr("10.0.0.254:1000".parse().unwrap()));
let ctx = c.into_request_context(&req);
assert_eq!(ctx.client_ip.as_deref(), Some("192.168.1.1"));
}
#[test]
fn test_default_converter_x_real_ip_overrides_forwarded_for() {
let c = DefaultRequestContextConverter::default()
.with_trusted_proxies(vec!["10.0.0.0/8".parse().unwrap()]);
let mut req = Request::builder()
.uri("/api")
.method("GET")
.header("X-Real-IP", "192.168.1.1")
.header("X-Forwarded-For", "10.0.0.1")
.body(())
.unwrap();
req.extensions_mut()
.insert(PeerAddr("10.0.0.254:1000".parse().unwrap()));
let ctx = c.into_request_context(&req);
assert_eq!(ctx.client_ip.as_deref(), Some("192.168.1.1"));
}
#[test]
fn test_default_converter_no_ip_headers() {
let c = DefaultRequestContextConverter::default();
let req = Request::builder()
.uri("/api")
.method("GET")
.body(())
.unwrap();
let ctx = c.into_request_context(&req);
assert!(ctx.client_ip.is_none());
}
#[test]
fn test_default_converter_headers_iterated() {
let c = DefaultRequestContextConverter::default();
let req = Request::builder()
.uri("/api")
.method("GET")
.header("X-Custom-Header", "custom-value")
.body(())
.unwrap();
let ctx = c.into_request_context(&req);
assert_eq!(ctx.headers.get("x-custom-header").unwrap(), "custom-value");
}
#[tokio::test]
async fn test_rate_limit_layer_with_converter() {
#[derive(Clone)]
struct CustomConverter;
impl<B> IntoRequestContext<B> for CustomConverter {
fn into_request_context(&self, _req: &Request<B>) -> RequestContext {
RequestContext::new()
.with_path("/custom")
.with_method("POST")
}
}
let (gov, _) = make_governor(gen_config(100, 10), true).await;
let layer =
RateLimitLayer::with_converter(gov, RateLimitConfig::default(), CustomConverter);
let mut svc: RateLimitService<MockService, CustomConverter> = layer.layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_layer_creates_service() {
let (gov, _) = make_governor(gen_config(100, 10), true).await;
let layer = RateLimitLayer::new(gov, RateLimitConfig::default());
let _svc: RateLimitService<MockService> = layer.layer(MockService);
}
#[tokio::test]
async fn test_service_health_check_path() {
let (gov, _) = make_governor(gen_config(1, 10), false).await;
let ctx = RequestContext::new()
.with_path("/api")
.with_method("GET")
.with_header("x-user-id", "u");
assert!(matches!(
gov.check(&ctx).await.unwrap(),
Decision::Allowed(_)
));
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(MockService);
let resp: Response<()> = svc.call(make_req("/health", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp.headers().get("RateLimit-Limit").is_none());
}
#[tokio::test]
async fn test_service_allowed() {
let (gov, _) = make_governor(gen_config(100, 10), false).await;
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(resp.headers().get("RateLimit-Limit").unwrap(), "0");
assert_eq!(resp.headers().get("RateLimit-Remaining").unwrap(), "0");
assert!(resp.headers().get("Retry-After").is_none());
}
#[tokio::test]
async fn test_service_rejected_with_429() {
let (gov, _) = make_governor(gen_config(1, 10), false).await;
let ctx = RequestContext::new()
.with_path("/api")
.with_method("GET")
.with_header("x-user-id", "u");
assert!(matches!(
gov.check(&ctx).await.unwrap(),
Decision::Allowed(_)
));
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
assert_eq!(resp.headers().get("RateLimit-Remaining").unwrap(), "0");
assert_eq!(resp.headers().get("Retry-After").unwrap(), "1");
}
#[tokio::test]
async fn test_service_rejected_pass_through() {
let (gov, _) = make_governor(gen_config(1, 10), false).await;
let ctx = RequestContext::new()
.with_path("/api")
.with_method("GET")
.with_header("x-user-id", "u");
assert!(matches!(
gov.check(&ctx).await.unwrap(),
Decision::Allowed(_)
));
let cfg = RateLimitConfig {
return_429_on_reject: false,
..RateLimitConfig::default()
};
let mut svc = RateLimitLayer::new(gov, cfg).layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp.headers().get("RateLimit-Limit").is_none());
}
#[cfg(feature = "parallel-checker")]
#[tokio::test]
async fn test_service_banned_with_403() {
use crate::storage::BanTarget;
use chrono::Utc;
let (gov, ban_storage) = make_governor(gen_config(100, 10), false).await;
let record = crate::storage::BanRecord {
target: BanTarget::UserId("u".into()),
ban_times: 1,
duration: std::time::Duration::from_secs(3600),
banned_at: Utc::now(),
expires_at: Utc::now() + chrono::Duration::hours(1),
is_manual: true,
reason: "test".into(),
};
ban_storage.save(&record).await.unwrap();
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
assert!(resp.headers().get("RateLimit-Limit").is_none());
}
#[cfg(feature = "parallel-checker")]
#[tokio::test]
async fn test_service_banned_pass_through() {
use crate::storage::BanTarget;
use chrono::Utc;
let (gov, ban_storage) = make_governor(gen_config(100, 10), false).await;
let record = crate::storage::BanRecord {
target: BanTarget::UserId("u".into()),
ban_times: 1,
duration: std::time::Duration::from_secs(3600),
banned_at: Utc::now(),
expires_at: Utc::now() + chrono::Duration::hours(1),
is_manual: true,
reason: "test".into(),
};
ban_storage.save(&record).await.unwrap();
let cfg = RateLimitConfig {
return_403_on_ban: false,
..RateLimitConfig::default()
};
let mut svc = RateLimitLayer::new(gov, cfg).layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert!(resp.headers().get("RateLimit-Limit").is_none());
}
#[tokio::test]
async fn test_service_poll_ready_ok() {
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
let (gov, _) = make_governor(gen_config(100, 10), false).await;
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(MockService);
let poll =
<RateLimitService<MockService> as Service<Request<()>>>::poll_ready(&mut svc, &mut cx);
match poll {
Poll::Ready(Ok(())) => {}
_ => panic!("expected Ready(Ok(()))"),
}
}
#[derive(Clone)]
struct ErrorMockService;
impl<B> Service<Request<B>> for ErrorMockService {
type Response = Response<()>;
type Error = BoxError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Err("service not ready".into()))
}
fn call(&mut self, _req: Request<B>) -> Self::Future {
Box::pin(async { Ok(Response::new(())) })
}
}
#[tokio::test]
async fn test_service_poll_ready_error() {
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
let (gov, _) = make_governor(gen_config(100, 10), false).await;
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default()).layer(ErrorMockService);
let poll = <RateLimitService<ErrorMockService> as Service<Request<()>>>::poll_ready(
&mut svc, &mut cx,
);
match poll {
Poll::Ready(Err(_)) => {}
_ => panic!("expected Ready(Err)"),
}
}
#[test]
fn test_reject_responder_default_snapshot() {
let got: Response<()> = IetfHttpResponder.reject_response(RejectInfo {
limit: Some(7),
reset_at: Some(1234567890),
retry_after: Some(9),
});
let mut expected = Response::new(());
*expected.status_mut() = StatusCode::TOO_MANY_REQUESTS;
let expected = inject_rate_limit_headers(
expected,
&RateLimitHeaderValues {
limit: 7,
remaining: 0,
reset_at: 1234567890,
retry_after: Some(9),
policy: String::new(),
},
);
assert_eq!(got.status(), expected.status());
assert_eq!(got.headers().len(), expected.headers().len());
for name in [
"RateLimit-Limit",
"RateLimit-Remaining",
"RateLimit-Reset",
"Retry-After",
"RateLimit-Policy",
] {
assert_eq!(
got.headers().get(name),
expected.headers().get(name),
"header {name} mismatch"
);
}
assert_eq!(got.headers().get("RateLimit-Limit").unwrap(), "7");
assert_eq!(got.headers().get("RateLimit-Remaining").unwrap(), "0");
assert_eq!(got.headers().get("RateLimit-Reset").unwrap(), "1234567890");
assert_eq!(got.headers().get("Retry-After").unwrap(), "9");
}
#[test]
fn test_reject_responder_default_sparse_info_bare_429() {
let got: Response<()> = IetfHttpResponder.reject_response(RejectInfo::default());
assert_eq!(got.status(), StatusCode::TOO_MANY_REQUESTS);
assert!(got.headers().get("RateLimit-Limit").is_none());
assert!(got.headers().get("RateLimit-Remaining").is_none());
assert!(got.headers().get("RateLimit-Reset").is_none());
assert!(got.headers().get("Retry-After").is_none());
}
#[tokio::test]
async fn test_service_rejected_uses_custom_responder() {
#[derive(Clone)]
struct CustomResponder {
calls: Arc<AtomicUsize>,
}
impl RejectResponder<()> for CustomResponder {
fn reject_response(&self, _info: RejectInfo) -> Response<()> {
self.calls.fetch_add(1, Ordering::SeqCst);
let mut resp = Response::new(());
*resp.status_mut() = StatusCode::IM_A_TEAPOT;
resp.headers_mut().insert(
"X-Custom-Responder",
http::header::HeaderValue::from_static("yes"),
);
resp
}
}
let (gov, _) = make_governor(gen_config(1, 10), false).await;
let calls = Arc::new(AtomicUsize::new(0));
let mut svc = RateLimitLayer::new(gov, RateLimitConfig::default())
.with_responder(CustomResponder {
calls: calls.clone(),
})
.layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(calls.load(Ordering::SeqCst), 0);
{
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::IM_A_TEAPOT);
assert_eq!(resp.headers().get("X-Custom-Responder").unwrap(), "yes");
assert!(resp.headers().get("RateLimit-Limit").is_none());
}
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn test_service_responder_not_called_when_429_disabled() {
#[derive(Clone)]
struct CountingResponder {
calls: Arc<AtomicUsize>,
}
impl RejectResponder<()> for CountingResponder {
fn reject_response(&self, _info: RejectInfo) -> Response<()> {
self.calls.fetch_add(1, Ordering::SeqCst);
Response::new(())
}
}
let (gov, _) = make_governor(gen_config(1, 10), false).await;
let calls = Arc::new(AtomicUsize::new(0));
let cfg = RateLimitConfig {
return_429_on_reject: false,
..RateLimitConfig::default()
};
let mut svc = RateLimitLayer::new(gov, cfg)
.with_responder(CountingResponder {
calls: calls.clone(),
})
.layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "u")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
calls.load(Ordering::SeqCst),
0,
"the pass-through path must not trigger the factory"
);
}
#[derive(Clone)]
struct StaticLimiter {
allow: Arc<AtomicBool>,
}
#[async_trait::async_trait]
impl crate::limiters::Limiter for StaticLimiter {
async fn allow(&self, _cost: u64) -> Result<bool, LimiteronError> {
Ok(self.allow.load(Ordering::SeqCst))
}
}
#[tokio::test]
async fn test_keyed_layer_allows_then_rejects_via_default_responder() {
let limiter = StaticLimiter {
allow: Arc::new(AtomicBool::new(true)),
};
let mut svc = KeyedRateLimitLayer::new(
Arc::new(limiter.clone()),
HeaderKeyExtractor::new("x-api-key"),
)
.layer(MockService);
let req = Request::builder()
.uri("/api")
.method("GET")
.header("X-API-Key", "k1")
.body(())
.unwrap();
let resp: Response<()> = svc.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
limiter.allow.store(false, Ordering::SeqCst);
let req = Request::builder()
.uri("/api")
.method("GET")
.header("X-API-Key", "k1")
.body(())
.unwrap();
let resp: Response<()> = svc.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
assert!(resp.headers().get("RateLimit-Limit").is_none());
}
#[tokio::test]
async fn test_keyed_layer_passes_through_without_key() {
let limiter = StaticLimiter {
allow: Arc::new(AtomicBool::new(false)),
};
let mut svc =
KeyedRateLimitLayer::new(Arc::new(limiter), HeaderKeyExtractor::new("x-api-key"))
.layer(MockService);
let req = Request::builder().uri("/api").method("GET").body(());
let resp: Response<()> = svc.call(req.unwrap()).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_keyed_layer_custom_responder_with_retry_after() {
#[derive(Clone)]
struct RetryAfterResponder {
calls: Arc<AtomicUsize>,
}
impl RejectResponder<()> for RetryAfterResponder {
fn reject_response(&self, info: RejectInfo) -> Response<()> {
self.calls.fetch_add(1, Ordering::SeqCst);
let mut resp = Response::new(());
*resp.status_mut() = StatusCode::TOO_MANY_REQUESTS;
if let Some(secs) = info.retry_after {
resp.headers_mut().insert(
"Retry-After",
http::header::HeaderValue::from_str(&secs.to_string()).unwrap(),
);
}
resp
}
}
let limiter = StaticLimiter {
allow: Arc::new(AtomicBool::new(false)),
};
let calls = Arc::new(AtomicUsize::new(0));
let mut svc = KeyedRateLimitLayer::with_responder(
Arc::new(limiter),
HeaderKeyExtractor::new("x-api-key"),
RetryAfterResponder {
calls: calls.clone(),
},
)
.layer(MockService);
let req = Request::builder()
.uri("/api")
.method("GET")
.header("X-API-Key", "k1")
.body(())
.unwrap();
let resp: Response<()> = svc.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[test]
fn test_header_key_extractor_borrows_short_values() {
use std::borrow::Cow;
let extractor = HeaderKeyExtractor::new("x-api-key");
let req = Request::builder()
.uri("/api")
.method("GET")
.header("X-API-Key", "short-key")
.body(())
.unwrap();
let key = extractor.request_key(&req).unwrap();
assert!(
matches!(key, Cow::Borrowed(_)),
"a short value should borrow zero-allocation"
);
assert_eq!(&*key, "short-key");
}
#[test]
fn test_header_key_extractor_truncates_long_values() {
let extractor = HeaderKeyExtractor::new("x-api-key");
let long = "a".repeat(300);
let req = Request::builder()
.uri("/api")
.method("GET")
.header("X-API-Key", &long)
.body(())
.unwrap();
let key = extractor.request_key(&req).unwrap();
assert_eq!(key.len(), HeaderKeyExtractor::DEFAULT_MAX_KEY_LEN);
assert_eq!(&*key, &long[..HeaderKeyExtractor::DEFAULT_MAX_KEY_LEN]);
let unlimited = extractor.with_max_key_len(0);
let key = unlimited.request_key(&req).unwrap();
assert_eq!(key.len(), 300);
}
#[test]
fn test_header_key_extractor_non_ascii_value_passes_through() {
let extractor = HeaderKeyExtractor::new("x-api-key");
let req = Request::builder()
.uri("/api")
.method("GET")
.header(
"X-API-Key",
http::header::HeaderValue::from_bytes("中文用户".as_bytes()).unwrap(),
)
.body(())
.unwrap();
assert!(extractor.request_key(&req).is_none());
}
#[test]
fn test_header_key_extractor_missing_header_is_none() {
let extractor = HeaderKeyExtractor::new("x-api-key");
let req = Request::builder()
.uri("/api")
.method("GET")
.body(())
.unwrap();
assert!(extractor.request_key(&req).is_none());
}
#[tokio::test]
async fn test_layer_converter_and_responder_combine() {
#[derive(Clone)]
struct FixedUserConverter;
impl<B> IntoRequestContext<B> for FixedUserConverter {
fn into_request_context(&self, _req: &Request<B>) -> RequestContext {
RequestContext::new()
.with_path("/api")
.with_method("GET")
.with_header("x-user-id", "combo-user")
}
}
#[derive(Clone)]
struct TeapotResponder;
impl RejectResponder<()> for TeapotResponder {
fn reject_response(&self, _info: RejectInfo) -> Response<()> {
let mut resp = Response::new(());
*resp.status_mut() = StatusCode::IM_A_TEAPOT;
resp
}
}
let (gov, _) = make_governor(gen_config(1, 10), false).await;
let mut svc =
RateLimitLayer::with_converter(gov, RateLimitConfig::default(), FixedUserConverter)
.with_responder(TeapotResponder)
.layer(MockService);
let resp: Response<()> = svc.call(make_req("/api", "anyone")).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let resp: Response<()> = svc.call(make_req("/api", "anyone")).await.unwrap();
assert_eq!(resp.status(), StatusCode::IM_A_TEAPOT);
}
}