mirror of
https://github.com/tokio-rs/axum.git
synced 2026-08-21 00:00:14 +02:00
feat: Introduce Bodies of Unknown Size
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<D> {
|
||||
_marker: PhantomData<fn() -> D>,
|
||||
}
|
||||
|
||||
impl<D> Unknown<D> {
|
||||
pub(crate) const fn new() -> Self {
|
||||
Self {
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Buf> Body for Unknown<D> {
|
||||
type Data = D;
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn poll_frame(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
|
||||
Poll::Ready(None)
|
||||
}
|
||||
|
||||
fn is_end_stream(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> SizeHint {
|
||||
SizeHint::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> fmt::Debug for Unknown<D> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Unknown").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Default for Unknown<D> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Clone for Unknown<D> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Copy for Unknown<D> {}
|
||||
@@ -128,7 +128,7 @@ impl IntoResponse for StatusCode {
|
||||
|
||||
impl IntoResponse for () {
|
||||
fn into_response(self) -> Response {
|
||||
Body::empty().into_response()
|
||||
Body::unknown().into_response()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String>` ([#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
|
||||
|
||||
|
||||
@@ -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::<Route<()>>();
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user