Hey,
While running the wasmtime p3 http components through jco, I hit a deadlock and I just wanted to sanity check a guest assumptions. The relevant code is in test-programs/src/p3/http.rs. It creates the request body stream like this:
let (mut contents_tx, contents_rx) = wit_stream::new();
let (trailers_tx, trailers_rx) = wit_future::new(|| Ok(None));
let (request, transmit) =
types::Request::new(headers, Some(contents_rx), trailers_rx, Some(options));
It then awaits client::send(request) before writing/dropping the request body stream. Only after a response is available it concurrently writes/drops the request body and collect the response:
let response = client::send(request).await?;
...
let ((), rx) = join!(
async {
if let Some(buf) = body {
let remaining = contents_tx.write_all(buf.into()).await;
assert!(remaining.is_empty());
}
drop(contents_tx);
_ = trailers_tx.write(Ok(None)).await;
},
async {
let body = body_rx.collect().await;
...
}
);
That works with the current wasmtime fixture server because the server sends the response without first waiting for the full request body. But I don't think that is a general http server requirement. A server may reasonably read request body chunks until request-body eof before sending response headers/status/body.
In that case the guest can deadlock:
client::send(request).awaiteof before sending a responsecontents_tx after client::send returnseof is never sent, so the server never respondsHere eof means the body terminator, not like tcp connection eof. For HTTP/1.1 chunked bodies, dropping/closing the body sender is what lets hyper send the terminating zero sized chunk. Until contents_tx is dropped, the server body stream can be open.
So I think the guest is assuming that client::send(request).await can complete before the request body producer completes. Is that intentional? If not, one possible fix would be to allow concurrent progress for sending the request body and waiting for the response, e.g. move the request-body write/drop path into the outer join! so it can run concurrently with client::send rather than only after send returns.
An example server loop that will cause guest to deadlock could look like this:
async fn handle(mut req: Request<Body>) -> Result<Response<Body>, Infallible> {
let mut chunks: Vec<Bytes> = Vec::new();
while let Some(next) = req.body_mut().data().await {
match next {
Ok(chunk) => chunks.push(chunk),
Err(err) => println!("error: {err}"),
}
}
...
}
I'm happy to prepare a PR if needed.
Yeah, I agree that the test is assuming too much about how and when the server will send a response; using join! to send the body concurrently with the call to client::send sounds good to me.
Last updated: Jun 01 2026 at 09:49 UTC