From 844ac717a461cfafbae1783bf50758867c89f5f2 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 31 Aug 2026 00:00:00 +0000 Subject: [PATCH 1/2] feat(util): introduce `ErrorBody` a common piece of boilerplate code that shows up in test coverage for body middleware is an "error body". at the time of writing, three such examples live in the test code for this respository. this commit introduces a reusable error body to facilitate authoring such test coverage. this body yields an error once, and then proceeds to yield `Ready(None)` when polled afterwards. this body exposes a `D` generic to control the `Body::Data` frame type, to permit this to be used in situations with various `Buf` implementations. Signed-off-by: katelyn martin --- http-body-util/src/error.rs | 103 ++++++++++++++++++++++++++++++++++++ http-body-util/src/lib.rs | 2 + 2 files changed, 105 insertions(+) create mode 100644 http-body-util/src/error.rs diff --git a/http-body-util/src/error.rs b/http-body-util/src/error.rs new file mode 100644 index 0000000..775e03d --- /dev/null +++ b/http-body-util/src/error.rs @@ -0,0 +1,103 @@ +use bytes::Buf; +use http_body::{Body, Frame, SizeHint}; +use std::{ + marker::PhantomData, + pin::Pin, + task::{Context, Poll}, +}; + +/// A [`Body`] that returns an error when polled. +/// +/// An [`ErrorBody`] is a body that will yield an `E`-typed error when [`Body::poll_frame()`] +/// is called. The `D` generic allows this body to be treated as a body that would yield a +/// particular [`Body::Data`] were it not to fail. +/// +/// This is most often useful for situations like exercising error-handling logic in tests. +#[derive(Debug)] +pub struct ErrorBody { + error: Option, + data: PhantomData, +} + +// === impl ErrorBody === + +impl ErrorBody { + /// Returns a new [`ErrorBody`] that will yield the provided error. + pub fn new(error: E) -> Self { + Self { + error: Some(error), + data: PhantomData, + } + } +} + +impl Body for ErrorBody +where + E: Unpin, + D: Buf + Unpin, +{ + type Data = D; + type Error = E; + + fn poll_frame( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + let Self { error, .. } = self.get_mut(); + + if let Some(error) = error.take() { + return Poll::Ready(Some(Err(error))); + } + + Poll::Ready(None) + } + + fn is_end_stream(&self) -> bool { + self.error.is_none() + } + + fn size_hint(&self) -> SizeHint { + SizeHint::default() + } +} + +#[cfg(test)] +mod error_body_tests { + use super::ErrorBody; + use bytes::Bytes; + use http_body::Body; + use std::{ + ops::Not, + pin::Pin, + task::{Context, Poll}, + }; + + #[test] + fn returns_error() { + type Error = &'static str; + + let mut body = ErrorBody::::new("problem"); + + assert!( + body.is_end_stream().not(), + "body is not finished until polled" + ); + assert_eq!(body.size_hint().lower(), 0); + assert_eq!(body.size_hint().upper(), None); + + let waker = futures_util::task::noop_waker(); + let mut cx = Context::from_waker(&waker); + + match Pin::new(&mut body).poll_frame(&mut cx) { + Poll::Ready(Some(Err("problem"))) => {} + other => panic!("unexpected poll outcome: {:?}", other), + } + + assert!(body.is_end_stream(), "body is finished after being polled"); + + match Pin::new(&mut body).poll_frame(&mut cx) { + Poll::Ready(None) => {} + other => panic!("unexpected poll outcome: {:?}", other), + } + } +} diff --git a/http-body-util/src/lib.rs b/http-body-util/src/lib.rs index ae2c369..547586f 100644 --- a/http-body-util/src/lib.rs +++ b/http-body-util/src/lib.rs @@ -12,6 +12,7 @@ mod collected; pub mod combinators; mod either; mod empty; +mod error; mod full; mod future; mod limited; @@ -27,6 +28,7 @@ use self::combinators::{BoxBody, MapErr, MapFrame, UnsyncBoxBody}; pub use self::collected::Collected; pub use self::either::Either; pub use self::empty::Empty; +pub use self::error::ErrorBody; pub use self::full::Full; pub use self::future::TryFutureBody; pub use self::limited::{LengthLimitError, Limited}; From aab6981379e02189b5a98ee9ec796dc4c19dd57b Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Mon, 31 Aug 2026 00:00:00 +0000 Subject: [PATCH 2/2] docs: add `ErrorBody::new` example because type inference can get tripped up on the `Body::Data` type, add an example showing the construction of an error body. Signed-off-by: katelyn martin --- http-body-util/src/error.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/http-body-util/src/error.rs b/http-body-util/src/error.rs index 775e03d..8d39f00 100644 --- a/http-body-util/src/error.rs +++ b/http-body-util/src/error.rs @@ -23,6 +23,23 @@ pub struct ErrorBody { impl ErrorBody { /// Returns a new [`ErrorBody`] that will yield the provided error. + /// + /// # Examples + /// + /// ``` + /// use bytes::Bytes; + /// use http_body_util::{BodyExt, ErrorBody}; + /// + /// #[tokio::main] + /// async fn main() { + /// let mut body = ErrorBody::::new("problem"); + /// let frame = body.frame().await; + /// assert_eq!( + /// frame.unwrap().unwrap_err(), + /// "problem", + /// ); + /// } + /// ``` pub fn new(error: E) -> Self { Self { error: Some(error),