Skip to main content

iroh_http_core/http/
body.rs

1//! Unified HTTP body type for `iroh-http-core`.
2//!
3//! Per [ADR-014](../../docs/adr/014-runtime-architecture.md) every HTTP body
4//! flowing through this crate — request bodies, response bodies, and bodies
5//! emerging from any tower-http layer — is wrapped in a single newtype:
6//! [`Body`]. This collapses the type-system tax that fallible middleware
7//! (compression, decompression, timeout) used to impose on the wiring code,
8//! and gives every layer a single concrete `B = Body` to compose against.
9//!
10//! The error type is intentionally [`BoxError`] (not `Infallible`) so that
11//! body adapters introduced by tower-http (decompression failures, timeout
12//! frame errors, etc.) can flow through the body without forcing the wiring
13//! to invent a new `B` parameter at every seam. All current body sources are
14//! infallible at construction time and convert into `BoxError` trivially.
15
16use std::pin::Pin;
17use std::task::{Context, Poll};
18
19use bytes::Bytes;
20use http_body::{Frame, SizeHint};
21use http_body_util::combinators::UnsyncBoxBody;
22use http_body_util::BodyExt;
23
24/// Boxed dynamic error used by [`Body`] and the serve service contract.
25pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
26
27/// Single HTTP body type used everywhere in `iroh-http-core`.
28///
29/// Wraps an [`UnsyncBoxBody`] of [`Bytes`] frames with a [`BoxError`] error
30/// channel. `Sync` is intentionally not required — neither hyper nor the
31/// tower-http layers we compose need it, and dropping it widens the set of
32/// body adapters we can box without ceremony.
33pub struct Body(UnsyncBoxBody<Bytes, BoxError>);
34
35impl Body {
36    /// An empty body (no frames, end-of-stream immediately).
37    pub fn empty() -> Self {
38        Self::new(http_body_util::Empty::<Bytes>::new())
39    }
40
41    /// A complete body of the given bytes, sent as a single frame.
42    pub fn full<B: Into<Bytes>>(bytes: B) -> Self {
43        Self::new(http_body_util::Full::new(bytes.into()))
44    }
45
46    /// Wrap any `http_body::Body` whose data are [`Bytes`] and whose error
47    /// converts into [`BoxError`].
48    ///
49    /// If the input is already a [`Body`], it is returned as-is without
50    /// an additional layer of boxing (fast path via [`try_downcast`]).
51    pub fn new<B>(body: B) -> Self
52    where
53        B: http_body::Body<Data = Bytes> + Send + 'static,
54        B::Error: Into<BoxError>,
55    {
56        try_downcast(body).unwrap_or_else(|body| Self(body.map_err(Into::into).boxed_unsync()))
57    }
58}
59
60/// Attempt to downcast a value of type `K` to type `T` using `Any`.
61/// Returns `Ok(T)` if the types match, `Err(K)` otherwise.
62#[allow(clippy::unwrap_used)] // Safety: both arms guarantee Some
63fn try_downcast<T: 'static, K: Send + 'static>(k: K) -> Result<T, K> {
64    let mut k = Some(k);
65    if let Some(k) = <dyn std::any::Any>::downcast_mut::<Option<T>>(&mut k) {
66        Ok(k.take().expect("downcast succeeded but value was None"))
67    } else {
68        Err(k.expect("downcast failed but value was None"))
69    }
70}
71
72impl Default for Body {
73    fn default() -> Self {
74        Self::empty()
75    }
76}
77
78impl http_body::Body for Body {
79    type Data = Bytes;
80    type Error = BoxError;
81
82    fn poll_frame(
83        self: Pin<&mut Self>,
84        cx: &mut Context<'_>,
85    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
86        Pin::new(&mut self.get_mut().0).poll_frame(cx)
87    }
88
89    fn is_end_stream(&self) -> bool {
90        self.0.is_end_stream()
91    }
92
93    fn size_hint(&self) -> SizeHint {
94        self.0.size_hint()
95    }
96}