use std::borrow::Cow;
use topcoat_core::context::{Cx, try_request_context};
use crate::{Body, IntoPath, Layer, LayerFuture, Next, Path};
pub(crate) const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024;
#[derive(Debug, Clone)]
#[must_use]
pub struct BodyLimit {
path: Cow<'static, Path>,
kind: BodyLimitKind,
}
impl BodyLimit {
pub const fn max(limit: usize) -> Self {
Self {
path: Cow::Borrowed(Path::ROOT),
kind: BodyLimitKind::Limit(limit),
}
}
pub const fn disable() -> Self {
Self {
path: Cow::Borrowed(Path::ROOT),
kind: BodyLimitKind::Disable,
}
}
#[track_caller]
pub fn at(mut self, path: impl IntoPath) -> Self {
self.path = path.into_path();
self
}
}
impl Layer for BodyLimit {
fn path(&self) -> Option<&Path> {
Some(&self.path)
}
fn handle<'a>(&'a self, cx: &'a Cx, body: Body, next: Next<'a>) -> LayerFuture<'a> {
let cx = cx.with(self.kind);
Box::pin(async move { next.run(&cx, body).await })
}
}
#[derive(Debug, Clone, Copy)]
enum BodyLimitKind {
Disable,
Limit(usize),
}
#[must_use]
pub fn body_limit(cx: &Cx) -> usize {
match try_request_context(cx) {
Some(BodyLimitKind::Limit(limit)) => *limit,
Some(BodyLimitKind::Disable) => usize::MAX,
None => DEFAULT_BODY_LIMIT,
}
}
#[cfg(test)]
mod tests {
use http::{Method, Request, StatusCode};
use topcoat_core::context::CxTestBuilder;
use super::*;
use crate::{
RouteFn, RouteFuture, Router,
request::{Bytes, FromRequest},
response::{IntoResponse, Response},
to_bytes,
};
#[test]
fn body_limit_defaults_to_two_mebibytes() {
assert_eq!(body_limit(&Cx::default()), DEFAULT_BODY_LIMIT);
}
#[test]
fn body_limit_reads_the_registered_limit() {
let cx = CxTestBuilder::new()
.request_context(BodyLimitKind::Limit(16))
.build();
assert_eq!(body_limit(&cx), 16);
}
#[test]
fn body_limit_disabled_is_unlimited() {
let cx = CxTestBuilder::new()
.request_context(BodyLimitKind::Disable)
.build();
assert_eq!(body_limit(&cx), usize::MAX);
}
#[test]
fn layer_defaults_to_the_root_path() {
assert_eq!(BodyLimit::max(1).path(), Some(Path::new("/")));
assert_eq!(BodyLimit::disable().path(), Some(Path::new("/")));
}
#[test]
fn at_scopes_the_layer_path() {
assert_eq!(
BodyLimit::max(1).at("/upload").path(),
Some(Path::new("/upload"))
);
}
fn echo(cx: &Cx, body: Body) -> RouteFuture<'_> {
Box::pin(async move {
let bytes = Bytes::from_request(cx, body).await?;
bytes.len().to_string().into_response(cx)
})
}
fn echo_route(path: &'static str) -> RouteFn {
RouteFn::new(Method::POST, path, echo)
}
async fn send(router: &Router, path: &str, size: usize) -> Response {
let request = Request::builder()
.method(Method::POST)
.uri(path)
.body(Body::from(vec![0u8; size]))
.expect("request should build");
router.handle(request).await
}
#[tokio::test]
async fn requests_within_the_default_limit_pass() {
let router = Router::builder().route(echo_route("/echo")).build();
assert_eq!(send(&router, "/echo", 1024).await.status(), StatusCode::OK);
}
#[tokio::test]
async fn requests_over_the_default_limit_are_content_too_large() {
let router = Router::builder().route(echo_route("/echo")).build();
let response = send(&router, "/echo", DEFAULT_BODY_LIMIT + 1).await;
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("reading the response body");
assert_eq!(&body[..], b"content too large");
}
#[tokio::test]
async fn max_overrides_the_default_limit() {
let router = Router::builder()
.route(echo_route("/echo"))
.layer(BodyLimit::max(8))
.build();
assert_eq!(send(&router, "/echo", 8).await.status(), StatusCode::OK);
assert_eq!(
send(&router, "/echo", 9).await.status(),
StatusCode::PAYLOAD_TOO_LARGE
);
}
#[tokio::test]
async fn disable_turns_the_limit_off() {
let router = Router::builder()
.route(echo_route("/echo"))
.layer(BodyLimit::disable())
.build();
let response = send(&router, "/echo", DEFAULT_BODY_LIMIT + 1).await;
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn a_scoped_layer_overrides_an_outer_one() {
let router = Router::builder()
.route(echo_route("/echo"))
.route(echo_route("/upload"))
.layer(BodyLimit::max(8))
.layer(BodyLimit::max(64).at("/upload"))
.build();
assert_eq!(send(&router, "/upload", 64).await.status(), StatusCode::OK);
assert_eq!(
send(&router, "/echo", 64).await.status(),
StatusCode::PAYLOAD_TOO_LARGE
);
}
#[tokio::test]
async fn the_raw_body_extractor_is_not_limited() {
fn raw(cx: &Cx, body: Body) -> RouteFuture<'_> {
Box::pin(async move {
let bytes = to_bytes(body, usize::MAX).await.expect("body reads fully");
bytes.len().to_string().into_response(cx)
})
}
let router = Router::builder()
.route(RouteFn::new(Method::POST, "/raw", raw))
.layer(BodyLimit::max(4))
.build();
assert_eq!(send(&router, "/raw", 1024).await.status(), StatusCode::OK);
}
}