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}