Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions http-body-util/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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<D, E>`] 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<D, E> {

@cratelyn cratelyn Sep 2, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚴 bikeshed: i could see this being called Error, rather than ErrorBody. because std::error::Error is such a foundational type though, and other common types like anyhow::Error and thiserror::Error would also conflict with that, i opted to include the Body suffix.

error: Option<E>,
data: PhantomData<D>,
}

// === impl ErrorBody ===

impl<D, E> ErrorBody<D, E> {
/// 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::<Bytes, &str>::new("problem");
/// let frame = body.frame().await;
/// assert_eq!(
/// frame.unwrap().unwrap_err(),
/// "problem",
/// );
/// }
/// ```
pub fn new(error: E) -> Self {
Self {
error: Some(error),
data: PhantomData,
}
}
}

impl<D, E> Body for ErrorBody<D, E>
where
E: Unpin,
D: Buf + Unpin,
{
type Data = D;
type Error = E;

fn poll_frame(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, 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::<Bytes, Error>::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),
}
}
}
2 changes: 2 additions & 0 deletions http-body-util/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod collected;
pub mod combinators;
mod either;
mod empty;
mod error;
mod full;
mod future;
mod limited;
Expand All @@ -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};
Expand Down