use std::sync::Arc;
use axum::body::Body;
use axum::extract::Request;
use axum::http::{header, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse as _, Response};
use axum::Router;
#[derive(Clone, Debug)]
pub struct BodyLimitLayer {
pub max_bytes: usize,
pub methods: Vec<axum::http::Method>,
}
impl Default for BodyLimitLayer {
fn default() -> Self {
Self::new(2 * 1024 * 1024)
}
}
impl BodyLimitLayer {
#[must_use]
pub fn new(max_bytes: usize) -> Self {
use axum::http::Method;
Self {
max_bytes,
methods: vec![
Method::POST,
Method::PUT,
Method::PATCH,
crate::http_query::QUERY.clone(),
],
}
}
#[must_use]
pub fn methods(mut self, m: Vec<axum::http::Method>) -> Self {
self.methods = m;
self
}
#[cfg(feature = "config")]
#[must_use]
pub fn from_settings(s: &crate::config::ServerSettings) -> Option<Self> {
let max = s.max_body_bytes?;
let max = usize::try_from(max).unwrap_or(usize::MAX);
Some(Self::new(max))
}
}
pub trait BodyLimitRouterExt {
#[must_use]
fn body_limit(self, layer: BodyLimitLayer) -> Self;
}
impl<S: Clone + Send + Sync + 'static> BodyLimitRouterExt for Router<S> {
fn body_limit(self, layer: BodyLimitLayer) -> Self {
let cfg = Arc::new(layer);
self.layer(axum::middleware::from_fn(
move |req: Request<Body>, next: Next| {
let cfg = cfg.clone();
async move { handle(cfg, req, next).await }
},
))
}
}
async fn handle(cfg: Arc<BodyLimitLayer>, mut req: Request<Body>, next: Next) -> Response {
if !cfg.methods.is_empty() && !cfg.methods.contains(req.method()) {
return next.run(req).await;
}
let cap = effective_cap(&mut req, cfg.max_bytes);
if let Some(declared) = req
.headers()
.get(header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
{
if usize::try_from(declared).map_or(true, |n| n > cap) {
return too_large(cap);
}
}
if req
.extensions()
.get::<StreamCap>()
.map_or(true, |c| cap < c.0)
{
req.extensions_mut().insert(StreamCap(cap));
req = req.map(|body| Body::new(http_body_util::Limited::new(body, cap)));
}
next.run(req).await
}
#[derive(Clone, Copy)]
struct StreamCap(usize);
#[cfg_attr(not(feature = "csrf"), allow(dead_code))]
pub(crate) fn stream_capped(extensions: &axum::http::Extensions) -> bool {
stream_cap(extensions).is_some()
}
#[cfg_attr(
not(any(feature = "csrf", feature = "admin", feature = "tenancy")),
allow(dead_code)
)]
pub(crate) fn stream_cap(extensions: &axum::http::Extensions) -> Option<usize> {
extensions.get::<StreamCap>().map(|c| c.0)
}
fn effective_cap(req: &mut Request<Body>, own: usize) -> usize {
use axum::extract::DefaultBodyLimit;
use axum::RequestExt as _;
let mut probe = Request::new(Body::new(Unsized));
*probe.extensions_mut() = std::mem::take(req.extensions_mut());
let before = probe.extensions().len();
let probe = probe.with_limited_body();
let outer = http_body::Body::size_hint(probe.body())
.upper()
.map(|n| usize::try_from(n).unwrap_or(usize::MAX));
let mut probe = probe.map(|_| ());
DefaultBodyLimit::disable().apply(&mut probe);
let set_outside = probe.extensions().len() == before;
*req.extensions_mut() = probe.into_parts().0.extensions;
let cap = match outer {
Some(n) if set_outside => n.min(own),
_ => own,
};
DefaultBodyLimit::max(cap).apply(req);
cap
}
struct Unsized;
impl http_body::Body for Unsized {
type Data = axum::body::Bytes;
type Error = std::convert::Infallible;
fn poll_frame(
self: std::pin::Pin<&mut Self>,
_: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
std::task::Poll::Ready(None)
}
}
#[cfg_attr(not(feature = "admin"), allow(dead_code))]
pub(crate) fn over_cap(e: &axum::Error) -> bool {
let mut err: Option<&(dyn std::error::Error + 'static)> = Some(e);
while let Some(e) = err {
if e.is::<http_body_util::LengthLimitError>() {
return true;
}
err = e.source();
}
false
}
#[cfg(feature = "admin")]
pub(crate) async fn collect_capped(
mut body: Body,
cap: usize,
) -> Result<Result<axum::body::Bytes, Body>, axum::Error> {
use http_body_util::BodyExt;
let mut buf = Vec::new();
while let Some(frame) = body.frame().await {
let Ok(data) = frame?.into_data() else {
continue;
};
if buf.len() + data.len() > cap {
let rest = Body::new(Prefixed {
head: Some(data),
rest: body,
});
if buf.is_empty() {
return Ok(Err(rest));
}
return Ok(Err(Body::new(Prefixed {
head: Some(axum::body::Bytes::from(buf)),
rest,
})));
}
buf.extend_from_slice(&data);
}
Ok(Ok(axum::body::Bytes::from(buf)))
}
#[cfg(feature = "admin")]
pub(crate) struct Prefixed {
pub(crate) head: Option<axum::body::Bytes>,
pub(crate) rest: Body,
}
#[cfg(feature = "admin")]
impl http_body::Body for Prefixed {
type Data = axum::body::Bytes;
type Error = axum::Error;
fn poll_frame(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Result<http_body::Frame<axum::body::Bytes>, axum::Error>>> {
if let Some(head) = self.head.take() {
return std::task::Poll::Ready(Some(Ok(http_body::Frame::data(head))));
}
std::pin::Pin::new(&mut self.rest).poll_frame(cx)
}
}
fn too_large(limit: usize) -> Response {
let status = StatusCode::PAYLOAD_TOO_LARGE;
let words = crate::errors::Message::new(crate::errors::error_words(status).1);
let body = crate::api_errors::ApiError::from_status(status, "payload too large")
.with_details(serde_json::json!({ "limit_bytes": limit }));
(words, body).into_response()
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::Method;
use axum::routing::{get, post};
use tower::ServiceExt;
fn app(limit: usize) -> Router {
Router::new()
.route("/", post(|| async { "ok" }))
.route("/get", get(|| async { "ok" }))
.body_limit(BodyLimitLayer::new(limit))
}
#[tokio::test]
async fn small_body_passes_through() {
let resp = app(1024)
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/")
.header(header::CONTENT_LENGTH, "10")
.body(Body::from("0123456789"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[cfg(feature = "admin")]
#[tokio::test]
async fn capped_collect_passes_the_crossing_frame_on_uncopied() {
use http_body_util::BodyExt as _;
let big = axum::body::Bytes::from(vec![7u8; 2 << 20]);
let ptr = big.as_ptr();
let Ok(Err(mut back)) = collect_capped(Body::from(big), 1 << 20).await else {
panic!("over the cap");
};
let frame = back.frame().await.unwrap().unwrap().into_data().unwrap();
assert_eq!((frame.as_ptr(), frame.len()), (ptr, 2 << 20));
assert!(back.frame().await.is_none());
}
#[tokio::test]
async fn body_at_exact_limit_passes() {
let resp = app(10)
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/")
.header(header::CONTENT_LENGTH, "10")
.body(Body::from("0123456789"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn body_over_limit_rejected_with_413_json() {
let resp = app(10)
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/")
.header(header::CONTENT_LENGTH, "100")
.body(Body::from("0".repeat(100)))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
assert_eq!(
resp.headers()
.get(header::CONTENT_TYPE)
.unwrap()
.to_str()
.unwrap(),
"application/json"
);
let bytes = axum::body::to_bytes(resp.into_body(), 1 << 16)
.await
.unwrap();
let v: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(v["error"], "payload_too_large");
assert_eq!(v["message"], "payload too large");
assert_eq!(v["details"]["limit_bytes"], 10);
}
#[tokio::test]
async fn get_requests_skipped_by_default() {
let resp = app(10)
.oneshot(
Request::builder()
.method(Method::GET)
.uri("/get")
.header(header::CONTENT_LENGTH, "999")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
fn streamed(method: Method, n: usize) -> Request<Body> {
Request::builder()
.method(method)
.uri("/")
.body(Body::from("0".repeat(n)))
.unwrap()
}
fn reading_app(limit: usize) -> Router {
Router::new()
.route("/", post(|b: String| async move { b.len().to_string() }))
.route("/q", crate::http_query::query(|b: String| async move { b }))
.body_limit(BodyLimitLayer::new(limit))
}
#[tokio::test]
async fn streamed_body_over_limit_gets_413() {
let resp = reading_app(1024)
.oneshot(streamed(Method::POST, 1025))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
async fn streamed_body_under_limit_passes() {
let resp = reading_app(1024)
.oneshot(streamed(Method::POST, 1024))
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn limit_above_axum_default_lets_the_extractor_read() {
let n = 3 * 1024 * 1024;
let resp = reading_app(5 * 1024 * 1024)
.oneshot(streamed(Method::POST, n))
.await
.unwrap();
assert_eq!(resp.status(), 200);
let mut req = streamed(Method::POST, n);
req.headers_mut()
.insert(header::CONTENT_LENGTH, n.to_string().parse().unwrap());
let resp = reading_app(5 * 1024 * 1024).oneshot(req).await.unwrap();
assert_eq!(resp.status(), 200);
}
#[tokio::test]
async fn a_stricter_outer_default_limit_is_kept() {
let app = reading_app(1024).layer(axum::extract::DefaultBodyLimit::max(10));
let resp = app.oneshot(streamed(Method::POST, 100)).await.unwrap();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
async fn an_outer_limit_equal_to_the_axum_default_is_kept() {
let app = reading_app(5 << 20).layer(axum::extract::DefaultBodyLimit::max(2 << 20));
let resp = app.oneshot(streamed(Method::POST, 3 << 20)).await.unwrap();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
async fn query_body_is_limited_by_default() {
let mut req = streamed(crate::http_query::QUERY.clone(), 200 * 1024);
*req.uri_mut() = "/q".parse().unwrap();
let resp = reading_app(1024).oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
async fn empty_methods_list_checks_every_method() {
let app = Router::new()
.route("/", get(|| async { "ok" }))
.body_limit(BodyLimitLayer::new(10).methods(Vec::new()));
let resp = app
.oneshot(
Request::builder()
.method(Method::GET)
.uri("/")
.header(header::CONTENT_LENGTH, "100")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[test]
fn default_layer_has_2mib_limit() {
let l = BodyLimitLayer::default();
assert_eq!(l.max_bytes, 2 * 1024 * 1024);
}
#[test]
fn default_methods_are_post_put_patch_query() {
let l = BodyLimitLayer::default();
assert_eq!(l.methods.len(), 4);
assert!(l.methods.contains(&Method::POST));
assert!(l.methods.contains(&Method::PUT));
assert!(l.methods.contains(&Method::PATCH));
assert!(l.methods.contains(&crate::http_query::QUERY));
}
#[cfg(feature = "config")]
#[test]
fn from_settings_unset_returns_none() {
let s = crate::config::ServerSettings::default();
assert!(BodyLimitLayer::from_settings(&s).is_none());
}
#[cfg(feature = "config")]
#[test]
fn from_settings_sets_max_bytes() {
let mut s = crate::config::ServerSettings::default();
s.max_body_bytes = Some(10_000_000); let layer = BodyLimitLayer::from_settings(&s).expect("Some");
assert_eq!(layer.max_bytes, 10_000_000);
assert_eq!(layer.methods, BodyLimitLayer::default().methods);
}
}