#![allow(clippy::disallowed_types)]
use bytes::Bytes;
use super::handles::{BodyReader, BodyWriter};
use crate::{http::body::BoxError, CoreError};
pub(crate) const PUMP_READ_BUF: usize = 64 * 1024;
fn is_length_limit_error(err: &(dyn std::error::Error + 'static)) -> bool {
let mut cur: Option<&(dyn std::error::Error + 'static)> = Some(err);
while let Some(e) = cur {
if e.is::<http_body_util::LengthLimitError>() {
return true;
}
cur = e.source();
}
false
}
pub(crate) async fn pump_quic_recv_to_body(
mut recv: iroh::endpoint::RecvStream,
writer: BodyWriter,
) {
while let Ok(Some(chunk)) = recv.read_chunk(PUMP_READ_BUF).await {
if writer.send_chunk(chunk).await.is_err() {
break;
}
}
}
pub(crate) async fn pump_body_to_quic_send(
reader: BodyReader,
mut send: iroh::endpoint::SendStream,
) {
loop {
match reader.next_chunk().await {
None => break,
Some(data) => {
if send.write_all(&data).await.is_err() {
break;
}
}
}
}
let _ = send.finish();
}
#[allow(dead_code)]
pub(crate) async fn pump_hyper_body_to_channel<B>(body: B, writer: BodyWriter)
where
B: http_body::Body<Data = Bytes>,
B::Error: Into<BoxError>,
{
let timeout = writer.drain_timeout;
pump_hyper_body_to_channel_limited(body, writer, None, timeout, None).await;
}
pub(crate) async fn pump_hyper_body_to_channel_limited<B>(
body: B,
writer: BodyWriter,
max_bytes: Option<usize>,
frame_timeout: std::time::Duration,
mut overflow_tx: Option<tokio::sync::oneshot::Sender<()>>,
) where
B: http_body::Body<Data = Bytes>,
B::Error: Into<BoxError>,
{
use http_body_util::BodyExt;
let mut body = Box::pin(body);
let mut total = 0usize;
let mut overflowed = false;
loop {
let frame_result = match tokio::time::timeout(frame_timeout, body.frame()).await {
Err(_elapsed) => {
tracing::warn!("iroh-http: body frame read timed out after {frame_timeout:?}");
writer.abort(CoreError::timeout("body frame read timed out"));
break;
}
Ok(None) => break,
Ok(Some(r)) => r,
};
match frame_result {
Err(e) => {
if overflowed {
break;
}
let boxed: BoxError = e.into();
if is_length_limit_error(&*boxed) {
tracing::warn!("iroh-http: request body exceeded configured limit: {boxed}");
writer.abort(CoreError::body_too_large(
"body exceeded configured size limit",
));
} else {
tracing::warn!("iroh-http: body frame error: {boxed:?}");
writer.abort(CoreError::internal(format!("body stream error: {boxed}")));
}
break;
}
Ok(frame) => {
if overflowed {
continue;
}
if frame.is_data() {
let data = frame.into_data().expect("is_data checked above");
total = total.saturating_add(data.len());
if let Some(limit) = max_bytes {
if total > limit {
tracing::warn!("iroh-http: request body exceeded {limit} bytes");
if let Some(tx) = overflow_tx.take() {
let _ = tx.send(());
}
writer.abort(CoreError::body_too_large(format!(
"body exceeded configured size limit of {limit} bytes"
)));
overflowed = true;
continue; }
}
if writer.send_chunk(data).await.is_err() {
return; }
}
}
}
}
drop(writer);
}
#[cfg(test)]
mod tests {
use super::super::handles::make_body_channel;
use super::*;
use http_body::Frame;
use http_body_util::StreamBody;
use std::convert::Infallible;
use std::time::Duration;
use tokio::sync::oneshot;
const FIFTY: &[u8] = &[b'x'; 50];
fn body_from_chunks(
chunks: Vec<&'static [u8]>,
) -> impl http_body::Body<Data = Bytes, Error = Infallible> {
let frames = chunks
.into_iter()
.map(|c| Ok::<_, Infallible>(Frame::data(Bytes::from_static(c))));
StreamBody::new(futures::stream::iter(frames))
}
#[tokio::test]
async fn pump_round_trip_forwards_all_chunks() {
let (writer, reader) = make_body_channel();
let body = body_from_chunks(vec![b"foo", b"bar", b"baz"]);
let pump = tokio::spawn(pump_hyper_body_to_channel_limited(
body,
writer,
None,
Duration::from_secs(5),
None,
));
let mut collected = Vec::new();
while let Some(chunk) = reader.next_chunk().await {
collected.extend_from_slice(&chunk);
}
pump.await.unwrap();
assert_eq!(collected, b"foobarbaz");
}
#[tokio::test]
async fn pump_over_limit_fires_overflow_and_drains_rest() {
let (writer, reader) = make_body_channel();
let (overflow_tx, overflow_rx) = oneshot::channel();
let body = body_from_chunks(vec![FIFTY, FIFTY, FIFTY, FIFTY]);
let pump = tokio::spawn(pump_hyper_body_to_channel_limited(
body,
writer,
Some(100),
Duration::from_secs(5),
Some(overflow_tx),
));
let mut total = 0usize;
while let Some(chunk) = reader.next_chunk().await {
total += chunk.len();
}
pump.await.unwrap();
assert_eq!(total, 100);
assert!(
overflow_rx.await.is_ok(),
"overflow signal should fire once"
);
}
#[tokio::test]
async fn pump_breaks_when_frame_read_times_out() {
let (writer, reader) = make_body_channel();
use futures::StreamExt;
let frames = futures::stream::iter(vec![Ok::<_, Infallible>(Frame::data(
Bytes::from_static(b"first"),
))])
.chain(futures::stream::pending());
let body = StreamBody::new(frames);
let start = std::time::Instant::now();
let pump = tokio::spawn(pump_hyper_body_to_channel_limited(
body,
writer,
None,
Duration::from_millis(50),
None,
));
let mut collected = Vec::new();
while let Some(chunk) = reader.next_chunk().await {
collected.extend_from_slice(&chunk);
}
pump.await.unwrap();
assert_eq!(collected, b"first");
assert!(
start.elapsed() < Duration::from_secs(2),
"pump should cut off the stalled body promptly",
);
}
#[tokio::test]
async fn pump_delivers_all_frames_under_slow_reader() {
let (writer, reader) = make_body_channel();
let body = body_from_chunks(vec![b"x".as_slice(); 50]);
let pump = tokio::spawn(pump_hyper_body_to_channel_limited(
body,
writer,
None,
Duration::from_secs(5),
None,
));
let mut count = 0usize;
while let Some(chunk) = reader.next_chunk().await {
count += chunk.len();
tokio::time::sleep(Duration::from_millis(1)).await;
}
pump.await.unwrap();
assert_eq!(count, 50);
}
#[tokio::test]
async fn pump_non_length_error_surfaces_internal_not_body_too_large() {
use http_body::Frame;
let (writer, reader) = make_body_channel();
let err = std::io::Error::other("connection reset by peer");
let frames = futures::stream::iter(vec![
Ok::<_, std::io::Error>(Frame::data(Bytes::from_static(b"partial"))),
Err(err),
]);
let body = StreamBody::new(frames);
let pump = tokio::spawn(pump_hyper_body_to_channel_limited(
body,
writer,
None,
Duration::from_secs(5),
None,
));
while reader.next_chunk().await.is_some() {}
pump.await.unwrap();
let code = reader
.terminal_error
.lock()
.unwrap()
.as_ref()
.map(|e| e.code);
assert_eq!(
code,
Some(crate::ErrorCode::Internal),
"non-size frame errors must not be reported as BodyTooLarge"
);
}
#[tokio::test]
async fn pump_length_limit_error_surfaces_body_too_large() {
use http_body_util::Limited;
let (writer, reader) = make_body_channel();
let inner = body_from_chunks(vec![FIFTY, FIFTY, FIFTY]);
let body = crate::Body::new(Limited::new(inner, 100));
let pump = tokio::spawn(pump_hyper_body_to_channel_limited(
body,
writer,
None,
Duration::from_secs(5),
None,
));
while reader.next_chunk().await.is_some() {}
pump.await.unwrap();
let code = reader
.terminal_error
.lock()
.unwrap()
.as_ref()
.map(|e| e.code);
assert_eq!(
code,
Some(crate::ErrorCode::BodyTooLarge),
"stack-enforced decoded cap (LengthLimitError) must map to BodyTooLarge"
);
}
#[tokio::test]
async fn pump_drain_time_error_preserves_body_too_large() {
use http_body::Frame;
let (writer, reader) = make_body_channel();
let err = std::io::Error::other("connection reset by peer");
let frames = futures::stream::iter(vec![
Ok::<_, std::io::Error>(Frame::data(Bytes::from_static(FIFTY))),
Ok(Frame::data(Bytes::from_static(FIFTY))),
Ok(Frame::data(Bytes::from_static(FIFTY))),
Err(err),
]);
let body = StreamBody::new(frames);
let pump = tokio::spawn(pump_hyper_body_to_channel_limited(
body,
writer,
Some(100),
Duration::from_secs(5),
None,
));
while reader.next_chunk().await.is_some() {}
pump.await.unwrap();
let code = reader
.terminal_error
.lock()
.unwrap()
.as_ref()
.map(|e| e.code);
assert_eq!(
code,
Some(crate::ErrorCode::BodyTooLarge),
"a drain-time stream error must not overwrite the BodyTooLarge terminal error"
);
}
#[tokio::test]
async fn pump_stops_when_reader_dropped() {
let (writer, reader) = make_body_channel();
drop(reader);
let body = body_from_chunks(vec![b"a", b"b", b"c"]);
let res = tokio::time::timeout(
Duration::from_secs(2),
pump_hyper_body_to_channel_limited(body, writer, None, Duration::from_secs(5), None),
)
.await;
assert!(
res.is_ok(),
"pump should terminate after the reader is dropped"
);
}
}