karthik-phl opened issue #12903:
Thanks for filing a bug report! Please fill out the TODOs below.
Note: if you want to report a security issue, please read our security policy!
Test Case
/// Body wrapper that emits a zero-length data frame mid-stream (before end-of-stream). /// /// This simulates what hyper/reqwest can do when the HTTP response data has been fully /// delivered but the framing (e.g., chunked transfer encoding) hasn't been finalized yet. /// In that state, `poll_frame` returns an empty data frame while `is_end_stream()` is /// still `false`. struct BodyWithEmptyFrameMidStream { inner: http_body_util::StreamBody< futures::channel::mpsc::Receiver<Result<http_body::Frame<Bytes>, ErrorCode>>, >, /// Whether to inject an empty frame before the next real frame inject_empty: bool, } impl http_body::Body for BodyWithEmptyFrameMidStream { type Data = Bytes; type Error = ErrorCode; fn poll_frame( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> { if self.inject_empty { self.inject_empty = false; return Poll::Ready(Some(Ok(http_body::Frame::data(Bytes::new())))); } let this = &mut *self; Pin::new(&mut this.inner).poll_frame(cx) } fn is_end_stream(&self) -> bool { // The key behavior: is_end_stream() returns false even when the empty frame // is emitted, because the stream hasn't been fully consumed yet. false } } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn p3_http_empty_frame_mid_stream() -> Result<()> { _ = env_logger::try_init(); // This test verifies the fix for the case where poll_frame returns a zero-length // data frame while is_end_stream() is false (i.e., mid-stream). Without the fix, // poll_produce would return StreamResult::Completed with 0 items produced, // violating the StreamProducer contract. let body = b"hello world"; let raw_body = Bytes::copy_from_slice(body); let (mut body_tx, body_rx) = futures::channel::mpsc::channel::<Result<_, ErrorCode>>(2); let wrapped_body = BodyWithEmptyFrameMidStream { inner: http_body_util::StreamBody::new(body_rx), inject_empty: true, // Inject an empty frame before the first real frame }; let request = http::Request::builder() .uri("http://localhost/") .method(http::Method::GET); let response = futures::join!( run_http( P3_HTTP_ECHO_COMPONENT, request.body(wrapped_body)?, oneshot::channel().0 ), async { body_tx .send(Ok(http_body::Frame::data(raw_body))) .await .unwrap(); drop(body_tx); } ) .0? .unwrap(); assert_eq!(response.status().as_u16(), 200); let (_, collected_body) = response.into_parts(); let collected_body = collected_body.to_bytes(); assert_eq!(collected_body, body.as_slice()); Ok(()) }Steps to Reproduce
A WASI HTTP guest making outbound HTTP requests (e.g., via
Reqwest) where either the upstream HTTP response body produces a zero-length data frame while is_end_stream() is still false, or poll_produce is called with a zero-capacity destination while the body still has data remainingTo reproduce deterministically, run the test
p3_http_empty_frame_mid_stream, above. It uses a custom BodyWithEmptyFrameMidStream wrapper that injects a single zero-length data frame before real data arrivesExpected Results
HostBodyStreamProducer::poll_produce should handle zero-length data frames and zero-capacity destinations gracefully — either re-polling for real data (returning Poll::Pending) or buffering, without ever returning StreamResult::Completed unless items were actually produced. The response body should be echoed back correctly.
Actual Results
poll_produce returns StreamResult::Completed without writing any data to the destination, violating the contract enforced in futures_and_streams.rs:2536–2544. This causes a trap with the message:
StreamProducer::poll_produce returned StreamResult::Completed without producing any items
This was observed in production with a WASI HTTP guest making concurrent outbound requests using futures::try_join!.Versions and Environment
Wasmtime version or commit: wasmtime
mainOperating system: TODO
Architecture: TODO
Extra Info
Anything else you'd like to add?
karthik-phl added the bug label to Issue #12903.
dicej closed issue #12903:
Thanks for filing a bug report! Please fill out the TODOs below.
Note: if you want to report a security issue, please read our security policy!
Test Case
/// Body wrapper that emits a zero-length data frame mid-stream (before end-of-stream). /// /// This simulates what hyper/reqwest can do when the HTTP response data has been fully /// delivered but the framing (e.g., chunked transfer encoding) hasn't been finalized yet. /// In that state, `poll_frame` returns an empty data frame while `is_end_stream()` is /// still `false`. struct BodyWithEmptyFrameMidStream { inner: http_body_util::StreamBody< futures::channel::mpsc::Receiver<Result<http_body::Frame<Bytes>, ErrorCode>>, >, /// Whether to inject an empty frame before the next real frame inject_empty: bool, } impl http_body::Body for BodyWithEmptyFrameMidStream { type Data = Bytes; type Error = ErrorCode; fn poll_frame( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> { if self.inject_empty { self.inject_empty = false; return Poll::Ready(Some(Ok(http_body::Frame::data(Bytes::new())))); } let this = &mut *self; Pin::new(&mut this.inner).poll_frame(cx) } fn is_end_stream(&self) -> bool { // The key behavior: is_end_stream() returns false even when the empty frame // is emitted, because the stream hasn't been fully consumed yet. false } } #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn p3_http_empty_frame_mid_stream() -> Result<()> { _ = env_logger::try_init(); // This test verifies the fix for the case where poll_frame returns a zero-length // data frame while is_end_stream() is false (i.e., mid-stream). Without the fix, // poll_produce would return StreamResult::Completed with 0 items produced, // violating the StreamProducer contract. let body = b"hello world"; let raw_body = Bytes::copy_from_slice(body); let (mut body_tx, body_rx) = futures::channel::mpsc::channel::<Result<_, ErrorCode>>(2); let wrapped_body = BodyWithEmptyFrameMidStream { inner: http_body_util::StreamBody::new(body_rx), inject_empty: true, // Inject an empty frame before the first real frame }; let request = http::Request::builder() .uri("http://localhost/") .method(http::Method::GET); let response = futures::join!( run_http( P3_HTTP_ECHO_COMPONENT, request.body(wrapped_body)?, oneshot::channel().0 ), async { body_tx .send(Ok(http_body::Frame::data(raw_body))) .await .unwrap(); drop(body_tx); } ) .0? .unwrap(); assert_eq!(response.status().as_u16(), 200); let (_, collected_body) = response.into_parts(); let collected_body = collected_body.to_bytes(); assert_eq!(collected_body, body.as_slice()); Ok(()) }Steps to Reproduce
A WASI HTTP guest making outbound HTTP requests (e.g., via
Reqwest) where either the upstream HTTP response body produces a zero-length data frame while is_end_stream() is still false, or poll_produce is called with a zero-capacity destination while the body still has data remainingTo reproduce deterministically, run the test
p3_http_empty_frame_mid_stream, above. It uses a custom BodyWithEmptyFrameMidStream wrapper that injects a single zero-length data frame before real data arrivesExpected Results
HostBodyStreamProducer::poll_produce should handle zero-length data frames and zero-capacity destinations gracefully — either re-polling for real data (returning Poll::Pending) or buffering, without ever returning StreamResult::Completed unless items were actually produced. The response body should be echoed back correctly.
Actual Results
poll_produce returns StreamResult::Completed without writing any data to the destination, violating the contract enforced in futures_and_streams.rs:2536–2544. This causes a trap with the message:
StreamProducer::poll_produce returned StreamResult::Completed without producing any items
This was observed in production with a WASI HTTP guest making concurrent outbound requests using futures::try_join!.Versions and Environment
Wasmtime version or commit: wasmtime
mainOperating system: TODO
Architecture: TODO
Extra Info
Anything else you'd like to add?
Last updated: Apr 12 2026 at 23:10 UTC