diff --git a/axum-core/CHANGELOG.md b/axum-core/CHANGELOG.md index ebeed0b9..e5cbe6c2 100644 --- a/axum-core/CHANGELOG.md +++ b/axum-core/CHANGELOG.md @@ -9,8 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **added:** `ResponseParts::status` and `ResponseParts::status_mut` accessors, allowing `IntoResponseParts` implementations to set the response status ([#3721]) +- **added:** `Body::unknown` to model a body of unknown size, which can be + helpful handling `HEAD` requests. ([#3742]) +- **changed:** `impl IntoResponse for ()` (which gets called by + `impl IntoResponse for HeaderMap`, `impl IntoResponse for Extensions` and + others) now returns a body of unknown size. [#3721]: https://github.com/tokio-rs/axum/pull/3721 +[#3742]: https://github.com/tokio-rs/axum/pull/3742 # 0.5.6 diff --git a/axum-core/src/body.rs b/axum-core/src/body.rs index cbc5606b..4e921c1e 100644 --- a/axum-core/src/body.rs +++ b/axum-core/src/body.rs @@ -1,5 +1,8 @@ //! HTTP body utilities. +mod unknown; +pub(crate) use unknown::Unknown; + use crate::{BoxError, Error}; use bytes::Bytes; use futures_core::{Stream, TryStream}; @@ -53,6 +56,30 @@ impl Body { Self::new(http_body_util::Empty::new()) } + /// Create a body of unknown size. + /// + /// This is useful in cases where a body is required to construct a + /// response, but the size of the body is not known. + /// + /// For example, this can be used to respond to `HEAD` requests, + /// for which the body of the corresponding `GET` request would be expensive + /// to compute. Note that this particular case is also mentioned in + /// [RFC 9110 (Section 9.3.2, Paragraph 2)]. + /// + /// The most notable difference compared to an empty body as returned by + /// [`Body::empty`] lies in the upper bound returned by [`Body::size_hint`]: + /// The upper bound of the size of an empty body is 0 bytes, while there + /// is no upper bound for the size of an unknown body. + /// + /// Other than the size hint, an unknown body behaves like an empty body, + /// i.e., [`Body::is_end_stream`] returns `true`, and when polled via + /// [`Body::poll_frame`], it immediately returns `Poll::Ready(None)`. + /// + /// [RFC 9110 (Section 9.3.2, Paragraph 2)]: https://datatracker.ietf.org/doc/html/rfc9110#section-9.3.2-2 + pub fn unknown() -> Self { + Self::new(Unknown::new()) + } + /// Create a new `Body` from a [`Stream`]. /// /// [`Stream`]: https://docs.rs/futures-core/latest/futures_core/stream/trait.Stream.html diff --git a/axum-core/src/body/unknown.rs b/axum-core/src/body/unknown.rs new file mode 100644 index 00000000..0d8da5ab --- /dev/null +++ b/axum-core/src/body/unknown.rs @@ -0,0 +1,63 @@ +use bytes::Buf; +use http_body::{Body, Frame, SizeHint}; +use std::{ + convert::Infallible, + fmt, + marker::PhantomData, + pin::Pin, + task::{Context, Poll}, +}; + +/// Refer to the documentation of [`super::Body::unknown`] which is `pub`. +pub(crate) struct Unknown { + _marker: PhantomData D>, +} + +impl Unknown { + pub(crate) const fn new() -> Self { + Self { + _marker: PhantomData, + } + } +} + +impl Body for Unknown { + type Data = D; + type Error = Infallible; + + #[inline] + fn poll_frame( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + Poll::Ready(None) + } + + fn is_end_stream(&self) -> bool { + true + } + + fn size_hint(&self) -> SizeHint { + SizeHint::default() + } +} + +impl fmt::Debug for Unknown { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Unknown").finish() + } +} + +impl Default for Unknown { + fn default() -> Self { + Self::new() + } +} + +impl Clone for Unknown { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Unknown {} diff --git a/axum-core/src/response/into_response.rs b/axum-core/src/response/into_response.rs index 5cb9187c..4fb4cf50 100644 --- a/axum-core/src/response/into_response.rs +++ b/axum-core/src/response/into_response.rs @@ -128,7 +128,7 @@ impl IntoResponse for StatusCode { impl IntoResponse for () { fn into_response(self) -> Response { - Body::empty().into_response() + Body::unknown().into_response() } } diff --git a/axum/CHANGELOG.md b/axum/CHANGELOG.md index ec68f076..dd3cead3 100644 --- a/axum/CHANGELOG.md +++ b/axum/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 type, not just `axum::body::Body` ([#3205]) - **changed:** `Redirect` constructors now accept any `impl Into` ([#3635]) - **changed:** Updated `matchit` allowing for routes with captures and static prefixes and suffixes ([#3702]) +- **fixed:** Responses to `HEAD` will not accidentally reply with `content-length: 0` anymore ([#3742]) [#3158]: https://github.com/tokio-rs/axum/pull/3158 [#3261]: https://github.com/tokio-rs/axum/pull/3261 @@ -35,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#3635]: https://github.com/tokio-rs/axum/pull/3635 [#3702]: https://github.com/tokio-rs/axum/pull/3702 [#3721]: https://github.com/tokio-rs/axum/pull/3721 +[#3742]: https://github.com/tokio-rs/axum/pull/3742 # 0.8.9 diff --git a/axum/src/routing/route.rs b/axum/src/routing/route.rs index deedb3a5..775a6a5b 100644 --- a/axum/src/routing/route.rs +++ b/axum/src/routing/route.rs @@ -239,10 +239,70 @@ impl Future for InfallibleRouteFuture { #[cfg(test)] mod tests { use super::*; + use crate::{routing::get, test_helpers::*, Router}; #[test] fn traits() { use crate::test_helpers::*; assert_send::>(); } + + #[crate::test] + async fn regression_3741() { + const BODY: &str = "Very expensive body."; + let content_length: HeaderValue = HeaderValue::from_str(&BODY.len().to_string()).unwrap(); + + async fn handler(method: http::Method) -> Response { + if method == http::Method::HEAD { + ().into_response() + } else { + BODY.into_response() + } + } + + let client = TestClient::new(Router::new().route("/", get(handler))); + + let get = client.get("/").await; + assert_eq!(get.status(), http::StatusCode::OK); + assert_eq!(get.headers().get(CONTENT_LENGTH), Some(&content_length)); + + let head = client.head("/").await; + assert_eq!(get.status(), http::StatusCode::OK); + assert_eq!(head.headers().get(CONTENT_LENGTH), None); + } + + #[crate::test] + async fn head_content_length_default() { + const BODY: &str = "Hello world!"; + let content_length: HeaderValue = HeaderValue::from_str(&BODY.len().to_string()).unwrap(); + + async fn handler() -> Response { + BODY.into_response() + } + + let client = TestClient::new(Router::new().route("/", get(handler))); + + let get = client.get("/").await; + assert_eq!(get.status(), http::StatusCode::OK); + assert_eq!(get.headers().get(CONTENT_LENGTH), Some(&content_length)); + + let head = client.head("/").await; + assert_eq!(head.status(), http::StatusCode::OK); + assert_eq!(head.headers().get(CONTENT_LENGTH), Some(&content_length)); + } + + #[crate::test] + async fn unit_content_length_zero() { + let content_length: HeaderValue = HeaderValue::from_static("0"); + + async fn handler() -> Response { + ().into_response() + } + + let client = TestClient::new(Router::new().route("/", get(handler))); + + let get = client.get("/").await; + assert_eq!(get.status(), http::StatusCode::OK); + assert_eq!(get.headers().get(CONTENT_LENGTH), Some(&content_length)); + } }