use std::task::{Context, Poll};
use axum::extract::Request;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use futures_util::FutureExt;
use futures_util::future::BoxFuture;
use tower::{Layer, Service};
const MAX_BODY_SIZE: usize = 1024 * 1024;
pub fn max_results_clamp(value: Option<i32>, max: usize) -> Option<usize> {
value.map(|v| (v as usize).min(max))
}
#[derive(Clone)]
pub struct ValidationMiddleware<S> {
inner: S,
}
impl<S> Service<Request> for ValidationMiddleware<S>
where
S: Service<Request, Response = Response> + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request) -> Self::Future {
if let Some(content_length) = req
.headers()
.get(axum::http::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<usize>().ok())
&& content_length > MAX_BODY_SIZE
{
let response = (
StatusCode::PAYLOAD_TOO_LARGE,
format!(
"Request body too large: {} bytes (limit: {} bytes)",
content_length, MAX_BODY_SIZE
),
)
.into_response();
return async { Ok(response) }.boxed();
}
self.inner.call(req).boxed()
}
}
#[derive(Clone, Default)]
pub struct ValidationLayer;
impl ValidationLayer {
pub fn new() -> Self {
Self
}
}
impl<S> Layer<S> for ValidationLayer {
type Service = ValidationMiddleware<S>;
fn layer(&self, inner: S) -> Self::Service {
ValidationMiddleware { inner }
}
}
#[cfg(test)]
mod tests {
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use tower::{ServiceBuilder, ServiceExt};
use super::*;
async fn echo(_req: Request<Body>) -> Result<Response<Body>, std::convert::Infallible> {
Ok(Response::new(Body::empty()))
}
#[tokio::test]
async fn test_allows_small_body() {
let mut svc = ServiceBuilder::new()
.layer(ValidationLayer::new())
.service_fn(echo);
let req = Request::post("/")
.header(header::CONTENT_LENGTH, "100")
.body(Body::empty())
.unwrap();
let resp = svc.ready().await.unwrap().call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_rejects_oversized_body() {
let mut svc = ServiceBuilder::new()
.layer(ValidationLayer::new())
.service_fn(echo);
let req = Request::post("/")
.header(header::CONTENT_LENGTH, (MAX_BODY_SIZE + 1).to_string())
.body(Body::empty())
.unwrap();
let resp = svc.ready().await.unwrap().call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
async fn test_allows_missing_content_length() {
let mut svc = ServiceBuilder::new()
.layer(ValidationLayer::new())
.service_fn(echo);
let req = Request::post("/").body(Body::empty()).unwrap();
let resp = svc.ready().await.unwrap().call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_max_results_clamp() {
assert_eq!(max_results_clamp(None, 200), None);
assert_eq!(max_results_clamp(Some(10), 200), Some(10));
assert_eq!(max_results_clamp(Some(200), 200), Some(200));
assert_eq!(max_results_clamp(Some(500), 200), Some(200));
assert_eq!(max_results_clamp(Some(0), 200), Some(0));
}
}