Skip to content

Conversation

@pchickey
Copy link
Contributor

@pchickey pchickey commented Sep 18, 2025

This is a major rewrite of the http api to provide compatibility with the http_body::Body trait. This trait is used throughout the Rust http ecosystem (e.g. hyper, reqwest, axum) to provide an async streaming representation of bodies with trailers.

There is a new wstd::http::Body struct that is a concrete representation for bodies.

The http server entrypoint now looks like:

#[wstd::http_server]
async fn main(request: Request<Body>) -> Result<Response<Body>, Error> {
   ...
}

http::Client now looks like

impl Client {
    pub async fn send<B: Into<Body>>(&self, req: Request<B>) -> Result<Response<Body>, Error> { ... }
   ...
}

There is no more IntoBody trait. To construct a Body, there are:

  • Body::empty for the empty body, or impl From<()> for Body
  • From<&[u8]> (which will make a clone) or From<Vec<u8>> or From<Bytes> for a Body from bytes.
  • From<&str> (which will make a clone) or From<String> for a Body from strings.
  • Body::from_json for a Body from a Serialize (requires feature json)
  • Body::from_stream or Body::from_try_stream for a Body from a Stream of Into<Bytes>
  • From<AsyncInputStream> for a Body with contents given by the contents of a WASI input-stream. Currently, this implementation goes by way of Body::from_try_stream(AsyncInputStream::into_stream()), which means it does not get the wstd::io::copy splicing optimization. This can be improved in the future.
  • internal to wstd, created from a wasi-http incoming-body resource. This variation allows efficient forwarding to an outgoing-body where body streaming can skip copying in and out of the guest, via wstd::io::copy.

The ways to consume a Body are:

  • Body::into_boxed_body converts it to an UnsyncBoxBody<Bytes, Error>. This is a boxed representation of http_body::Body that is Send but not Sync. The Unsync variant is required for compatibility with the axum crate.
  • async fn Body::contents(&mut self) -> Result<&[u8], Error> is ready when all contents of the body have been collected, and gives them as a byte slice.
  • async fn Body::str_contents(&mut self) -> Result<&str, Error> is ready when all contents of the body have been collected, and gives them as a str slice.
  • async fn Body::json(&mut self) -> Result<T, Error> gathers body contents and then uses T: serde::Deserialize to deserialize to json (requires feature json).

The http server and client examples and their test harnesses have been rewritten and expanded as part of this effort.

@pchickey pchickey force-pushed the pch/server_result_body branch from 512947a to c1451ba Compare October 10, 2025 00:29
@pchickey pchickey force-pushed the pch/server_result_body branch from da75dda to b5ad19c Compare October 15, 2025 21:49
@pchickey pchickey force-pushed the pch/server_result_body branch from b5ad19c to 5d82cd8 Compare October 15, 2025 21:58
@sunfishcode
Copy link
Member

The previous BodyForthcoming mechanism allowed one to start sending the response and then stream out a body computed on the fly. If I understand correctly, the way to do that with this PR is to effectively spawn an async block. Is there an example of doing this, and streaming to the response? It seems like the http_wait_body example is close, however it computes the body as a single Bytes rather than a stream.

@pchickey
Copy link
Contributor Author

pchickey commented Oct 16, 2025

There's no spawning required in order to produce a streaming body.

The http_body::Body trait's poll_frame is used to produce body chunks. It has an interface kinda like Future::poll, but it is challenging to use on its own. http_body_util contains a number of useful constructors and methods that operate on Body.

http_body::Body is pretty close to Stream, and StreamBody provides an adapter from Stream to Body. That opens up a number of other useful ways to construct and combine streams.

Unfortunately, because Body is implemented in terms of a poll_ fn, its not trivial to hook it up to async {} blocks or async fn members of other traits. One consequence of this is that, while we can have a special case for handling an AsyncInputStream that bypasses all of the http_body::Body machinery to use wasi stream splicing directly, its pretty difficult and inefficient to use an impl AsyncRead to produce a Body with the current AsyncRead definition in terms of async fn read(&mut self, ...) -> .... So, unfortunately, we will probably have to change AsyncRead to have a fn poll_read(Pin<&mut Self>, ...) -> Poll<...> form, in order to provide efficient interop. (This is not particularly related to your question, but its something I've been wrestling with the last few days, and spent a bunch of time talking to Yosh about recently.)

@pchickey
Copy link
Contributor Author

I added a more interesting streaming body example.
demo

@pchickey pchickey changed the title DRAFT: use http_body::Body use http_body::Body Oct 16, 2025
@pchickey pchickey force-pushed the pch/server_result_body branch from aea7ac5 to 85c2ab7 Compare October 16, 2025 23:20
@pchickey pchickey force-pushed the pch/server_result_body branch from 85c2ab7 to 7c0b015 Compare October 16, 2025 23:26
Copy link
Member

@sunfishcode sunfishcode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I haven't studied the implementation here in detail, but I think the big picture here of integrating with the Rust http_body crate makes sense.

@pchickey pchickey mentioned this pull request Oct 17, 2025
…cWrite traits

because the traits have ?Send bounds, but the actual AsyncInputStream
and AsyncOutputStream are both Send, so when we bypass the traits Rust
can prove that the Future for splicing is Send.

Same with write_all: when used through AsyncWrite its ?Send, but when
used on AsyncOutputStream the Future for write_all is Send.

Together, these changes allow us to show Rust that the Body::send and
http::Client::send futures are Send.
@pchickey pchickey merged commit dab8e47 into main Oct 23, 2025
5 checks passed
@yoshuawuyts yoshuawuyts deleted the pch/server_result_body branch October 23, 2025 17:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants