#![allow(clippy::disallowed_types)]
mod common;
use std::task::Poll;
use std::time::Duration;
use bytes::Bytes;
use iroh_http_core::{
fetch, ffi_serve, respond, IrohEndpoint, NetworkingOptions, NodeOptions, RequestPayload,
ServeOptions,
};
use tokio::sync::Notify;
async fn bind_client() -> IrohEndpoint {
IrohEndpoint::bind(NodeOptions {
networking: NetworkingOptions {
disabled: true,
bind_addrs: vec!["127.0.0.1:0".into()],
..Default::default()
},
..Default::default()
})
.await
.unwrap()
}
async fn await_handle_baseline(ep: &IrohEndpoint, baseline: usize) -> usize {
let mut total = ep.handles().count_handles().3;
for _ in 0..200 {
if total <= baseline + 1 {
return total;
}
ep.sweep_now();
tokio::time::sleep(Duration::from_millis(10)).await;
total = ep.handles().count_handles().3;
}
total
}
#[tokio::test]
async fn backpressure_slow_consumer_blocks_sender() {
let ep = bind_client().await;
let handles = ep.handles();
let (writer, reader) = handles.alloc_body_writer().unwrap();
let mut buffered = 0usize;
let blocked = loop {
let mut fut = Box::pin(handles.send_chunk(writer, Bytes::from_static(b"chunk")));
match futures::poll!(fut.as_mut()) {
Poll::Ready(res) => {
res.expect("buffered send should succeed while slots remain");
buffered += 1;
assert!(
buffered <= 1024,
"sender buffered {buffered} chunks without backpressure — unbounded growth"
);
}
Poll::Pending => break fut,
}
};
assert!(
buffered > 0,
"expected at least one chunk to buffer before the sender parked"
);
let first = reader.next_chunk().await;
assert!(
first.is_some(),
"consumer should read the first buffered chunk"
);
tokio::time::timeout(Duration::from_secs(5), blocked)
.await
.expect("parked send should complete once the consumer frees a slot")
.expect("send should succeed after a slot is freed");
}
#[tokio::test]
async fn handle_count_returns_to_baseline_after_churn() {
let (server_ep, client_ep) = common::make_pair().await;
let server_id = common::node_id(&server_ep);
let addrs = common::server_addrs(&server_ep);
let serve_ep = server_ep.clone();
let _handle = ffi_serve(
server_ep.clone(),
ServeOptions::default(),
move |payload: RequestPayload| {
let ep = serve_ep.clone();
tokio::spawn(async move {
let req_h = payload.req_handle;
let req_body_h = payload.req_body_handle;
let res_h = payload.res_body_handle;
if req_body_h != 0 {
while ep
.handles()
.next_chunk(req_body_h)
.await
.ok()
.flatten()
.is_some()
{}
}
respond(
ep.handles(),
req_h,
200,
vec![("content-length".into(), "2".into())],
)
.unwrap();
ep.handles()
.send_chunk(res_h, Bytes::from_static(b"ok"))
.await
.unwrap();
ep.handles().finish_body(res_h).unwrap();
});
},
);
let server_baseline = server_ep.handles().count_handles().3;
let client_baseline = client_ep.handles().count_handles().3;
for _ in 0..20 {
let res = fetch(
&client_ep,
&server_id,
"/echo",
"GET",
&[],
None,
None,
Some(&addrs),
None,
true,
None,
)
.await
.expect("fetch should succeed");
assert_eq!(res.status, 200);
while client_ep
.handles()
.next_chunk(res.body_handle)
.await
.unwrap()
.is_some()
{}
}
let server_total = await_handle_baseline(&server_ep, server_baseline).await;
assert!(
server_total <= server_baseline + 1,
"server leaked handles: {server_total} (baseline {server_baseline})"
);
let client_total = await_handle_baseline(&client_ep, client_baseline).await;
assert!(
client_total <= client_baseline + 1,
"client leaked handles: {client_total} (baseline {client_baseline})"
);
}
#[tokio::test]
async fn midstream_client_disconnect_errors_server_and_cleans_up() {
let (server_ep, client_ep) = common::make_pair().await;
let server_id = common::node_id(&server_ep);
let addrs = common::server_addrs(&server_ep);
let streaming = std::sync::Arc::new(Notify::new());
let streaming_tx = streaming.clone();
let send_errored = std::sync::Arc::new(Notify::new());
let send_errored_tx = send_errored.clone();
let serve_ep = server_ep.clone();
let server_baseline = server_ep.handles().count_handles().3;
let _handle = ffi_serve(
server_ep.clone(),
ServeOptions::default(),
move |payload: RequestPayload| {
let ep = serve_ep.clone();
let started = streaming_tx.clone();
let errored = send_errored_tx.clone();
tokio::spawn(async move {
let req_h = payload.req_handle;
let res_h = payload.res_body_handle;
respond(ep.handles(), req_h, 200, vec![]).unwrap();
started.notify_one();
let chunk = Bytes::from(vec![0u8; 16 * 1024]);
loop {
if ep.handles().send_chunk(res_h, chunk.clone()).await.is_err() {
errored.notify_one();
break;
}
}
let _ = ep.handles().finish_body(res_h);
});
},
);
let res = fetch(
&client_ep,
&server_id,
"/stream",
"GET",
&[],
None,
None,
Some(&addrs),
None,
true,
None,
)
.await
.expect("fetch head should arrive");
assert_eq!(res.status, 200);
streaming.notified().await;
for _ in 0..2 {
let _ = client_ep.handles().next_chunk(res.body_handle).await;
}
client_ep.close_force().await;
tokio::time::timeout(Duration::from_secs(15), send_errored.notified())
.await
.expect("server send_chunk should error after the client disconnects");
let server_total = await_handle_baseline(&server_ep, server_baseline).await;
assert!(
server_total <= server_baseline + 1,
"server leaked handles after disconnect: {server_total} (baseline {server_baseline})"
);
}
#[tokio::test]
async fn accept_loop_survives_connect_reset_flood() {
let (server_ep, clean_client) = common::make_pair().await;
let server_id = common::node_id(&server_ep);
let addrs = common::server_addrs(&server_ep);
let serve_ep = server_ep.clone();
let _handle = ffi_serve(
server_ep.clone(),
ServeOptions::default(),
move |payload: RequestPayload| {
let ep = serve_ep.clone();
tokio::spawn(async move {
let req_h = payload.req_handle;
let res_h = payload.res_body_handle;
respond(
ep.handles(),
req_h,
200,
vec![("content-length".into(), "2".into())],
)
.unwrap();
let _ = ep
.handles()
.send_chunk(res_h, Bytes::from_static(b"ok"))
.await;
let _ = ep.handles().finish_body(res_h);
});
},
);
let mut floods = Vec::new();
for _ in 0..16 {
let sid = server_id.clone();
let a = addrs.clone();
floods.push(tokio::spawn(async move {
let c = bind_client().await;
let _ = tokio::time::timeout(
Duration::from_millis(50),
fetch(
&c,
&sid,
"/",
"GET",
&[],
None,
None,
Some(&a),
None,
true,
None,
),
)
.await;
c.close_force().await;
}));
}
for f in floods {
let _ = f.await;
}
let res = tokio::time::timeout(
Duration::from_secs(15),
fetch(
&clean_client,
&server_id,
"/",
"GET",
&[],
None,
None,
Some(&addrs),
None,
true,
None,
),
)
.await
.expect("clean fetch should not hang after the flood")
.expect("clean fetch should succeed after the flood");
assert_eq!(res.status, 200);
while clean_client
.handles()
.next_chunk(res.body_handle)
.await
.unwrap()
.is_some()
{}
}