Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically handle http_body::LengthLimitError #1048

Merged
merged 9 commits into from Jun 8, 2022
6 changes: 6 additions & 0 deletions axum-core/CHANGELOG.md
Expand Up @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

# Unreleased

- **added:** Automatically handle `http_body::LengthLimitError` in `FailedToBufferBody` and map
such errors to `413 Payload Too Large` ([#1048])
- **added:** `FailedToBufferBody::is_length_limit_error` to check if the underlying error is
`http_body::LengthLimitError`. Its source error can also be downcast to
`http_body::LengthLimitError` ([#1048])
- **fixed:** Use `impl IntoResponse` less in docs ([#1049])

[#1048]: https://github.com/tokio-rs/axum/pull/1048
[#1049]: https://github.com/tokio-rs/axum/pull/1049

# 0.2.4 (02. May, 2022)
Expand Down
2 changes: 1 addition & 1 deletion axum-core/Cargo.toml
Expand Up @@ -15,7 +15,7 @@ async-trait = "0.1"
bytes = "1.0"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
http = "0.2.7"
http-body = "0.4"
http-body = "0.4.5"
mime = "0.3.16"

[dev-dependencies]
Expand Down
75 changes: 69 additions & 6 deletions axum-core/src/extract/rejection.rs
Expand Up @@ -2,6 +2,7 @@

use crate::response::{IntoResponse, Response};
use http::StatusCode;
use http_body::LengthLimitError;
use std::fmt;

/// Rejection type used if you try and extract the request body more than
Expand All @@ -28,12 +29,74 @@ impl fmt::Display for BodyAlreadyExtracted {

impl std::error::Error for BodyAlreadyExtracted {}

define_rejection! {
#[status = BAD_REQUEST]
#[body = "Failed to buffer the request body"]
/// Rejection type for extractors that buffer the request body. Used if the
/// request body cannot be buffered due to an error.
pub struct FailedToBufferBody(Error);
/// Rejection type for extractors that buffer the request body. Used if the
/// request body cannot be buffered due to an error.
// TODO: in next major for axum-core make this a #[non_exhaustive] enum so we don't need the
// additional indirection
jplatte marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Debug)]
pub struct FailedToBufferBody(FailedToBufferBodyInner);

impl FailedToBufferBody {
/// Check if the body failed to be buffered because a length limit was hit.
///
/// This can _only_ happen when you're using [`tower_http::limit::RequestBodyLimitLayer`] or
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
/// otherwise wrapping request bodies in [`http_body::Limited`].
///
/// [`tower_http::limit::RequestBodyLimitLayer`]: https://docs.rs/tower-http/latest/tower_http/limit/struct.RequestBodyLimitLayer.html
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
pub fn is_length_limit_error(&self) -> bool {
matches!(self.0, FailedToBufferBodyInner::LengthLimitError(_))
}
}

#[derive(Debug)]
enum FailedToBufferBodyInner {
Unknown(crate::Error),
LengthLimitError(LengthLimitError),
}

impl FailedToBufferBody {
pub(crate) fn from_err<E>(err: E) -> Self
where
E: Into<crate::BoxError>,
{
let err = err.into();
match err.downcast::<LengthLimitError>() {
Ok(err) => Self(FailedToBufferBodyInner::LengthLimitError(*err)),
Err(err) => Self(FailedToBufferBodyInner::Unknown(crate::Error::new(err))),
}
}
}

impl crate::response::IntoResponse for FailedToBufferBody {
fn into_response(self) -> crate::response::Response {
match self.0 {
FailedToBufferBodyInner::Unknown(err) => (
http::StatusCode::BAD_REQUEST,
format!(concat!("Failed to buffer the request body", ": {}"), err),
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
)
.into_response(),
FailedToBufferBodyInner::LengthLimitError(err) => (
StatusCode::PAYLOAD_TOO_LARGE,
format!(concat!("Failed to buffer the request body", ": {}"), err),
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
)
.into_response(),
}
}
}

impl std::fmt::Display for FailedToBufferBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Failed to buffer the request body")
}
}

impl std::error::Error for FailedToBufferBody {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.0 {
FailedToBufferBodyInner::Unknown(err) => Some(err),
FailedToBufferBodyInner::LengthLimitError(err) => Some(err),
}
}
}

define_rejection! {
Expand Down
2 changes: 1 addition & 1 deletion axum/Cargo.toml
Expand Up @@ -81,7 +81,7 @@ features = [
]

[dev-dependencies.tower-http]
version = "0.3.0"
version = "0.3.4"
features = ["full"]

[package.metadata.docs.rs]
Expand Down
5 changes: 5 additions & 0 deletions axum/src/extract/content_length_limit.rs
Expand Up @@ -29,6 +29,11 @@ use std::ops::Deref;
/// ```
///
/// This requires the request to have a `Content-Length` header.
///
/// If you want to limit the size of request bodies without requiring a `Content-Length` header
davidpdrsn marked this conversation as resolved.
Show resolved Hide resolved
/// consider using [`tower_http::limit::RequestBodyLimitLayer`].
// TODO(david): change this to apply `http_body::Limited` so it also supports streaming requests.
// Will probably require renaming it.
#[derive(Debug, Clone)]
pub struct ContentLengthLimit<T, const N: u64>(pub T);

Expand Down
17 changes: 17 additions & 0 deletions axum/src/routing/tests/mod.rs
Expand Up @@ -699,3 +699,20 @@ async fn routes_must_start_with_slash() {
let app = Router::new().route(":foo", get(|| async {}));
TestClient::new(app);
}

#[tokio::test]
async fn limited_body() {
const LIMIT: usize = 3;

let app = Router::new()
.route("/", post(|_: Bytes| async {}))
.layer(tower_http::limit::RequestBodyLimitLayer::new(LIMIT));

let client = TestClient::new(app);

let res = client.post("/").body("a".repeat(LIMIT)).send().await;
assert_eq!(res.status(), StatusCode::OK);

let res = client.post("/").body("a".repeat(LIMIT * 2)).send().await;
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
}