mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-28 00:00:20 +02:00
Remove ContentLengthLimit (#1400)
* feat: remove ContentLengthLimit * feat: remove ContentLengthLimit rejections * fix: update multipart docs * fix: typo * feat: add wip extractor code * feat: revert "feat: add wip extractor code" * fix: update Multipart docs * fix: update examples * fix: missing import in an example * fix: broken import yet again * fix: disable default body limit for example * fix: key value store example * fix: update expected debug_handler output * chore: update CHANGELOG * Update axum/CHANGELOG.md Co-authored-by: David Pedersen <[email protected]>
This commit is contained in:
co-authored by
David Pedersen
parent
c3f3db79ec
commit
896ffc5fba
@@ -1,274 +0,0 @@
|
||||
use super::{rejection::*, FromRequest};
|
||||
use async_trait::async_trait;
|
||||
use axum_core::{extract::FromRequestParts, response::IntoResponse};
|
||||
use http::{request::Parts, Method, Request};
|
||||
use http_body::Limited;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Extractor that will reject requests with a body larger than some size.
|
||||
///
|
||||
/// `GET`, `HEAD`, and `OPTIONS` requests are rejected if they have a `Content-Length` header,
|
||||
/// otherwise they're accepted without the body being checked.
|
||||
///
|
||||
/// Note: `ContentLengthLimit` can wrap types that extract the body (for example, [`Form`] or [`Json`])
|
||||
/// if that is the case, the inner type will consume the request's body, which means the
|
||||
/// `ContentLengthLimit` must come *last* if the handler uses several extractors. See
|
||||
/// ["the order of extractors"][order-of-extractors]
|
||||
///
|
||||
/// [order-of-extractors]: crate::extract#the-order-of-extractors
|
||||
/// [`Form`]: crate::form::Form
|
||||
/// [`Json`]: crate::json::Json
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use axum::{
|
||||
/// extract::ContentLengthLimit,
|
||||
/// routing::post,
|
||||
/// Router,
|
||||
/// };
|
||||
///
|
||||
/// async fn handler(body: ContentLengthLimit<String, 1024>) {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// let app = Router::new().route("/", post(handler));
|
||||
/// # async {
|
||||
/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
|
||||
/// # };
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContentLengthLimit<T, const N: u64>(pub T);
|
||||
|
||||
#[async_trait]
|
||||
impl<T, S, B, R, const N: u64> FromRequest<S, B> for ContentLengthLimit<T, N>
|
||||
where
|
||||
T: FromRequest<S, B, Rejection = R> + FromRequest<S, Limited<B>, Rejection = R>,
|
||||
R: IntoResponse + Send,
|
||||
B: Send + 'static,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = ContentLengthLimitRejection<R>;
|
||||
|
||||
async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let (parts, body) = req.into_parts();
|
||||
|
||||
let value = if let Some(err) = validate::<N>(&parts).err() {
|
||||
match err {
|
||||
RequestValidationError::LengthRequiredStream => {
|
||||
// `Limited` supports limiting streams, so use that instead since this is a
|
||||
// streaming request
|
||||
let body = Limited::new(body, N as usize);
|
||||
let req = Request::from_parts(parts, body);
|
||||
T::from_request(req, state)
|
||||
.await
|
||||
.map_err(ContentLengthLimitRejection::Inner)?
|
||||
}
|
||||
other => return Err(other.into()),
|
||||
}
|
||||
} else {
|
||||
let req = Request::from_parts(parts, body);
|
||||
T::from_request(req, state)
|
||||
.await
|
||||
.map_err(ContentLengthLimitRejection::Inner)?
|
||||
};
|
||||
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T, S, const N: u64> FromRequestParts<S> for ContentLengthLimit<T, N>
|
||||
where
|
||||
T: FromRequestParts<S>,
|
||||
T::Rejection: IntoResponse,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = ContentLengthLimitRejection<T::Rejection>;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
validate::<N>(parts)?;
|
||||
|
||||
let value = T::from_request_parts(parts, state)
|
||||
.await
|
||||
.map_err(ContentLengthLimitRejection::Inner)?;
|
||||
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
fn validate<const N: u64>(parts: &Parts) -> Result<(), RequestValidationError> {
|
||||
let content_length = parts
|
||||
.headers
|
||||
.get(http::header::CONTENT_LENGTH)
|
||||
.and_then(|value| value.to_str().ok()?.parse::<u64>().ok());
|
||||
|
||||
match (content_length, &parts.method) {
|
||||
(content_length, &(Method::GET | Method::HEAD | Method::OPTIONS)) => {
|
||||
if content_length.is_some() {
|
||||
return Err(RequestValidationError::ContentLengthNotAllowed);
|
||||
} else if parts
|
||||
.headers
|
||||
.get(http::header::TRANSFER_ENCODING)
|
||||
.map_or(false, |value| value.as_bytes() == b"chunked")
|
||||
{
|
||||
return Err(RequestValidationError::LengthRequiredChunkedHeadOrGet);
|
||||
}
|
||||
}
|
||||
(Some(content_length), _) if content_length > N => {
|
||||
return Err(RequestValidationError::PayloadTooLarge);
|
||||
}
|
||||
(None, _) => {
|
||||
return Err(RequestValidationError::LengthRequiredStream);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<T, const N: u64> Deref for ContentLengthLimit<T, N> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to `ContentLengthLimitRejection` but more fine grained in that we can tell the
|
||||
/// difference between `LengthRequiredStream` and `LengthRequiredChunkedHeadOrGet`
|
||||
enum RequestValidationError {
|
||||
PayloadTooLarge,
|
||||
LengthRequiredStream,
|
||||
LengthRequiredChunkedHeadOrGet,
|
||||
ContentLengthNotAllowed,
|
||||
}
|
||||
|
||||
impl<T> From<RequestValidationError> for ContentLengthLimitRejection<T> {
|
||||
fn from(inner: RequestValidationError) -> Self {
|
||||
match inner {
|
||||
RequestValidationError::PayloadTooLarge => Self::PayloadTooLarge(PayloadTooLarge),
|
||||
RequestValidationError::LengthRequiredStream
|
||||
| RequestValidationError::LengthRequiredChunkedHeadOrGet => {
|
||||
Self::LengthRequired(LengthRequired)
|
||||
}
|
||||
RequestValidationError::ContentLengthNotAllowed => {
|
||||
Self::ContentLengthNotAllowed(ContentLengthNotAllowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
body::Bytes,
|
||||
routing::{get, post},
|
||||
test_helpers::*,
|
||||
Router,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[tokio::test]
|
||||
async fn body_with_length_limit() {
|
||||
use std::iter::repeat;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Input {
|
||||
foo: String,
|
||||
}
|
||||
|
||||
const LIMIT: u64 = 8;
|
||||
|
||||
let app = Router::new().route(
|
||||
"/",
|
||||
post(|_body: ContentLengthLimit<Bytes, LIMIT>| async {}),
|
||||
);
|
||||
|
||||
let client = TestClient::new(app);
|
||||
let res = client
|
||||
.post("/")
|
||||
.body(repeat(0_u8).take((LIMIT - 1) as usize).collect::<Vec<_>>())
|
||||
.send()
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client
|
||||
.post("/")
|
||||
.body(repeat(0_u8).take(LIMIT as usize).collect::<Vec<_>>())
|
||||
.send()
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let res = client
|
||||
.post("/")
|
||||
.body(repeat(0_u8).take((LIMIT + 1) as usize).collect::<Vec<_>>())
|
||||
.send()
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
||||
|
||||
let chunk = repeat(0_u8).take(LIMIT as usize).collect::<Bytes>();
|
||||
let res = client
|
||||
.post("/")
|
||||
.body(reqwest::Body::wrap_stream(futures_util::stream::iter(
|
||||
vec![Ok::<_, std::io::Error>(chunk)],
|
||||
)))
|
||||
.send()
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let chunk = repeat(0_u8).take((LIMIT + 1) as usize).collect::<Bytes>();
|
||||
let res = client
|
||||
.post("/")
|
||||
.body(reqwest::Body::wrap_stream(futures_util::stream::iter(
|
||||
vec![Ok::<_, std::io::Error>(chunk)],
|
||||
)))
|
||||
.send()
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_request_without_content_length_is_accepted() {
|
||||
let app = Router::new().route("/", get(|_body: ContentLengthLimit<Bytes, 1337>| async {}));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client.get("/").send().await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_request_with_content_length_is_rejected() {
|
||||
let app = Router::new().route("/", get(|_body: ContentLengthLimit<Bytes, 1337>| async {}));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client
|
||||
.get("/")
|
||||
.header("content-length", 3)
|
||||
.body("foo")
|
||||
.send()
|
||||
.await;
|
||||
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_request_with_chunked_encoding_is_rejected() {
|
||||
let app = Router::new().route("/", get(|_body: ContentLengthLimit<Bytes, 1337>| async {}));
|
||||
|
||||
let client = TestClient::new(app);
|
||||
|
||||
let res = client
|
||||
.get("/")
|
||||
.header("transfer-encoding", "chunked")
|
||||
.body("3\r\nfoo\r\n0\r\n\r\n")
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ pub mod rejection;
|
||||
#[cfg(feature = "ws")]
|
||||
pub mod ws;
|
||||
|
||||
mod content_length_limit;
|
||||
mod host;
|
||||
mod raw_query;
|
||||
mod request_parts;
|
||||
@@ -25,7 +24,6 @@ pub use axum_macros::{FromRequest, FromRequestParts};
|
||||
#[allow(deprecated)]
|
||||
pub use self::{
|
||||
connect_info::ConnectInfo,
|
||||
content_length_limit::ContentLengthLimit,
|
||||
host::Host,
|
||||
path::Path,
|
||||
raw_query::RawQuery,
|
||||
|
||||
@@ -49,7 +49,8 @@ use std::{
|
||||
/// ```
|
||||
///
|
||||
/// For security reasons it's recommended to combine this with
|
||||
/// [`ContentLengthLimit`](super::ContentLengthLimit) to limit the size of the request payload.
|
||||
/// [`RequestBodyLimitLayer`](tower_http::limit::RequestBodyLimitLayer)
|
||||
/// to limit the size of the request payload.
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "multipart")))]
|
||||
#[derive(Debug)]
|
||||
pub struct Multipart {
|
||||
|
||||
@@ -47,30 +47,6 @@ define_rejection! {
|
||||
pub struct MissingExtension(Error);
|
||||
}
|
||||
|
||||
define_rejection! {
|
||||
#[status = PAYLOAD_TOO_LARGE]
|
||||
#[body = "Request payload is too large"]
|
||||
/// Rejection type for [`ContentLengthLimit`](super::ContentLengthLimit) if
|
||||
/// the request body is too large.
|
||||
pub struct PayloadTooLarge;
|
||||
}
|
||||
|
||||
define_rejection! {
|
||||
#[status = LENGTH_REQUIRED]
|
||||
#[body = "Content length header is required"]
|
||||
/// Rejection type for [`ContentLengthLimit`](super::ContentLengthLimit) if
|
||||
/// the request is missing the `Content-Length` header or it is invalid.
|
||||
pub struct LengthRequired;
|
||||
}
|
||||
|
||||
define_rejection! {
|
||||
#[status = BAD_REQUEST]
|
||||
#[body = "`GET`, `HEAD`, `OPTIONS` requests are not allowed to have a `Content-Length` header"]
|
||||
/// Rejection type for [`ContentLengthLimit`](super::ContentLengthLimit) if
|
||||
/// the request is `GET`, `HEAD`, or `OPTIONS` and has a `Content-Length` header.
|
||||
pub struct ContentLengthNotAllowed;
|
||||
}
|
||||
|
||||
define_rejection! {
|
||||
#[status = INTERNAL_SERVER_ERROR]
|
||||
#[body = "No paths parameters found for matched route"]
|
||||
@@ -216,64 +192,5 @@ composite_rejection! {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rejection used for [`ContentLengthLimit`](super::ContentLengthLimit).
|
||||
///
|
||||
/// Contains one variant for each way the
|
||||
/// [`ContentLengthLimit`](super::ContentLengthLimit) extractor can fail.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ContentLengthLimitRejection<T> {
|
||||
#[allow(missing_docs)]
|
||||
PayloadTooLarge(PayloadTooLarge),
|
||||
#[allow(missing_docs)]
|
||||
LengthRequired(LengthRequired),
|
||||
#[allow(missing_docs)]
|
||||
ContentLengthNotAllowed(ContentLengthNotAllowed),
|
||||
#[allow(missing_docs)]
|
||||
Inner(T),
|
||||
}
|
||||
|
||||
impl<T> IntoResponse for ContentLengthLimitRejection<T>
|
||||
where
|
||||
T: IntoResponse,
|
||||
{
|
||||
fn into_response(self) -> Response {
|
||||
match self {
|
||||
Self::PayloadTooLarge(inner) => inner.into_response(),
|
||||
Self::LengthRequired(inner) => inner.into_response(),
|
||||
Self::ContentLengthNotAllowed(inner) => inner.into_response(),
|
||||
Self::Inner(inner) => inner.into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Display for ContentLengthLimitRejection<T>
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::PayloadTooLarge(inner) => inner.fmt(f),
|
||||
Self::LengthRequired(inner) => inner.fmt(f),
|
||||
Self::ContentLengthNotAllowed(inner) => inner.fmt(f),
|
||||
Self::Inner(inner) => inner.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::error::Error for ContentLengthLimitRejection<T>
|
||||
where
|
||||
T: std::error::Error + 'static,
|
||||
{
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::PayloadTooLarge(inner) => Some(inner),
|
||||
Self::LengthRequired(inner) => Some(inner),
|
||||
Self::ContentLengthNotAllowed(inner) => Some(inner),
|
||||
Self::Inner(inner) => Some(inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "headers")]
|
||||
pub use crate::typed_header::{TypedHeaderRejection, TypedHeaderRejectionReason};
|
||||
|
||||
Reference in New Issue
Block a user