use xecute::exceptions::RequestId;
use crate::{
configs::Config,
server::{error, rate_limit::RateLimiter},
};
use axum::{
extract::{ConnectInfo, Request, State},
http::{HeaderValue, Method, StatusCode, header},
middleware::Next,
response::Response,
};
use std::{net::SocketAddr, sync::Arc};
use tokio::sync::Semaphore;
pub(crate) struct GuardState {
limiter: RateLimiter,
permits: Arc<Semaphore>,
timeout: std::time::Duration,
}
pub(crate) async fn guard(
State(state): State<Arc<GuardState>>,
request: Request,
next: Next,
) -> Response {
let healthy = request.method() == Method::GET
&& matches!(request.uri().path(), "/health/live" | "/health/ready");
let request_id = request
.extensions()
.get::<RequestId>()
.map(|id| id.0.as_str())
.unwrap_or("");
let permit = if healthy {
None
} else {
let Some(ConnectInfo(peer)) = request.extensions().get::<ConnectInfo<SocketAddr>>() else {
tracing::error!("socket peer missing; use into_make_service_with_connect_info");
return error::response(StatusCode::INTERNAL_SERVER_ERROR, request_id);
};
if let Err(retry) = state.limiter.check(peer.ip()) {
let mut response = error::response(StatusCode::TOO_MANY_REQUESTS, request_id);
response.headers_mut().insert(
header::RETRY_AFTER,
HeaderValue::from_str(&retry.to_string()).unwrap(),
);
return response;
}
match state.permits.clone().try_acquire_owned() {
Ok(permit) => Some(permit),
Err(_) => {
let mut response = error::response(StatusCode::SERVICE_UNAVAILABLE, request_id);
response
.headers_mut()
.insert(header::RETRY_AFTER, HeaderValue::from_static("1"));
return response;
}
}
};
let id = request_id.to_owned();
let response = match tokio::time::timeout(state.timeout, next.run(request)).await {
Ok(response) => response,
Err(_) => error::response(StatusCode::GATEWAY_TIMEOUT, &id),
};
drop(permit);
response
}
pub(crate) fn state(config: &Config) -> Arc<GuardState> {
Arc::new(GuardState {
limiter: RateLimiter::new(
config.rate_limit_requests,
config.rate_limit_window,
config.rate_limit_max_clients,
),
permits: Arc::new(Semaphore::new(config.max_in_flight)),
timeout: config.request_timeout,
})
}