From e781d6bb84fad9e5b955927ce6d14a691e1eeb73 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 8 Aug 2025 14:45:40 -0700 Subject: [PATCH 01/13] add some futures_core::stream::Stream impls for AsyncInputStream --- src/io/streams.rs | 140 +++++++++++++++++++++++++++++++++++++---- src/runtime/reactor.rs | 2 + 2 files changed, 131 insertions(+), 11 deletions(-) diff --git a/src/io/streams.rs b/src/io/streams.rs index d7139c1..c437dfa 100644 --- a/src/io/streams.rs +++ b/src/io/streams.rs @@ -1,6 +1,8 @@ use super::{AsyncPollable, AsyncRead, AsyncWrite}; use std::cell::OnceCell; -use std::io::Result; +use std::future::{poll_fn, Future}; +use std::pin::Pin; +use std::task::{Context, Poll}; use wasi::io::streams::{InputStream, OutputStream, StreamError}; /// A wrapper for WASI's `InputStream` resource that provides implementations of `AsyncRead` and @@ -21,18 +23,23 @@ impl AsyncInputStream { stream, } } - /// Await for read readiness. - async fn ready(&self) { + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<()> { // Lazily initialize the AsyncPollable let subscription = self .subscription .get_or_init(|| AsyncPollable::new(self.stream.subscribe())); // Wait on readiness - subscription.wait_for().await; + let wait_for = subscription.wait_for(); + let mut pinned = std::pin::pin!(wait_for); + pinned.as_mut().poll(cx) + } + /// Await for read readiness. + async fn ready(&self) { + poll_fn(|cx| self.poll_ready(cx)).await } /// Asynchronously read from the input stream. /// This method is the same as [`AsyncRead::read`], but doesn't require a `&mut self`. - pub async fn read(&self, buf: &mut [u8]) -> Result { + pub async fn read(&self, buf: &mut [u8]) -> std::io::Result { let read = loop { self.ready().await; // Ideally, the ABI would be able to read directly into buf. @@ -56,10 +63,40 @@ impl AsyncInputStream { buf[0..len].copy_from_slice(&read); Ok(len) } + + /// Use this `AsyncInputStream` as a `futures_core::stream::Stream` with + /// items of `Result, std::io::Error>`. The returned byte vectors + /// will be at most 8k. If you want to control chunk size, use + /// `Self::into_stream_of`. + pub fn into_stream(self) -> AsyncInputChunkStream { + AsyncInputChunkStream { + stream: self, + chunk_size: 8 * 1024, + } + } + + /// Use this `AsyncInputStream` as a `futures_core::stream::Stream` with + /// items of `Result, std::io::Error>`. The returned byte vectors + /// will be at most the `chunk_size` argument specified. + pub fn into_stream_of(self, chunk_size: usize) -> AsyncInputChunkStream { + AsyncInputChunkStream { + stream: self, + chunk_size, + } + } + + /// Use this `AsyncInputStream` as a `futures_core::stream::Stream` with + /// items of `Result`. + pub fn into_bytestream(self) -> AsyncInputByteStream { + AsyncInputByteStream { + stream: self.into_stream(), + buffer: std::io::Read::bytes(std::io::Cursor::new(Vec::new())), + } + } } impl AsyncRead for AsyncInputStream { - async fn read(&mut self, buf: &mut [u8]) -> Result { + async fn read(&mut self, buf: &mut [u8]) -> std::io::Result { Self::read(self, buf).await } @@ -69,6 +106,87 @@ impl AsyncRead for AsyncInputStream { } } +/// Wrapper of `AsyncInputStream` that impls `futures_core::stream::Stream` +/// with an item of `Result, std::io::Error>` +pub struct AsyncInputChunkStream { + stream: AsyncInputStream, + chunk_size: usize, +} + +impl AsyncInputChunkStream { + /// Extract the `AsyncInputStream` which backs this stream. + pub fn into_inner(self) -> AsyncInputStream { + self.stream + } +} + +impl futures_core::stream::Stream for AsyncInputChunkStream { + type Item = Result, std::io::Error>; + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.stream.poll_ready(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(()) => match self.stream.stream.read(self.chunk_size as u64) { + Ok(r) if r.is_empty() => Poll::Pending, + Ok(r) => Poll::Ready(Some(Ok(r))), + Err(StreamError::LastOperationFailed(err)) => { + Poll::Ready(Some(Err(std::io::Error::other(err.to_debug_string())))) + } + Err(StreamError::Closed) => Poll::Ready(None), + }, + } + } +} + +pin_project_lite::pin_project! { + /// Wrapper of `AsyncInputStream` that impls + /// `futures_core::stream::Stream` with item `Result`. + pub struct AsyncInputByteStream { + #[pin] + stream: AsyncInputChunkStream, + buffer: std::io::Bytes>>, + } +} + +impl AsyncInputByteStream { + /// Extract the `AsyncInputStream` which backs this stream, and any bytes + /// read from the `AsyncInputStream` which have not yet been yielded by + /// the byte stream. + pub fn into_inner(self) -> (AsyncInputStream, Vec) { + ( + self.stream.into_inner(), + self.buffer + .collect::, std::io::Error>>() + .expect("read of Cursor> is infallible"), + ) + } +} + +impl futures_core::stream::Stream for AsyncInputByteStream { + type Item = Result; + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + match this.buffer.next() { + Some(byte) => Poll::Ready(Some(Ok(byte.expect("cursor on Vec is infallible")))), + None => match futures_core::stream::Stream::poll_next(this.stream, cx) { + Poll::Ready(Some(Ok(bytes))) => { + let mut bytes = std::io::Read::bytes(std::io::Cursor::new(bytes)); + match bytes.next() { + Some(Ok(byte)) => { + *this.buffer = bytes; + Poll::Ready(Some(Ok(byte))) + } + Some(Err(err)) => Poll::Ready(Some(Err(err))), + None => Poll::Ready(None), + } + } + Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + }, + } + } +} + /// A wrapper for WASI's `output-stream` resource that provides implementations of `AsyncWrite` and /// `AsyncPollable`. #[derive(Debug)] @@ -104,7 +222,7 @@ impl AsyncOutputStream { /// a `std::io::Error` indicating either an error returned by the stream write /// using the debug string provided by the WASI error, or else that the, /// indicated by `std::io::ErrorKind::ConnectionReset`. - pub async fn write(&self, buf: &[u8]) -> Result { + pub async fn write(&self, buf: &[u8]) -> std::io::Result { // Loops at most twice. loop { match self.stream.check_write() { @@ -145,7 +263,7 @@ impl AsyncOutputStream { /// the stream flush, using the debug string provided by the WASI error, /// or else that the stream is closed, indicated by /// `std::io::ErrorKind::ConnectionReset`. - pub async fn flush(&self) -> Result<()> { + pub async fn flush(&self) -> std::io::Result<()> { match self.stream.flush() { Ok(()) => { self.ready().await; @@ -162,10 +280,10 @@ impl AsyncOutputStream { } impl AsyncWrite for AsyncOutputStream { // Required methods - async fn write(&mut self, buf: &[u8]) -> Result { + async fn write(&mut self, buf: &[u8]) -> std::io::Result { Self::write(self, buf).await } - async fn flush(&mut self) -> Result<()> { + async fn flush(&mut self) -> std::io::Result<()> { Self::flush(self).await } @@ -180,7 +298,7 @@ pub(crate) async fn splice( reader: &AsyncInputStream, writer: &AsyncOutputStream, len: u64, -) -> core::result::Result { +) -> Result { // Wait for both streams to be ready. let r = reader.ready(); writer.ready().await; diff --git a/src/runtime/reactor.rs b/src/runtime/reactor.rs index 8462bd9..e6d1a2b 100644 --- a/src/runtime/reactor.rs +++ b/src/runtime/reactor.rs @@ -38,6 +38,8 @@ impl AsyncPollable { pub fn new(pollable: Pollable) -> Self { Reactor::current().schedule(pollable) } + // TODO: can I instead return a Pin<&mut WaitFor> here? so we dont keep + // recreating this. /// Create a Future that waits for the Pollable's readiness. pub fn wait_for(&self) -> WaitFor { use std::sync::atomic::{AtomicUsize, Ordering}; From e41312af6f8f08d4edc19859b95b7478c306afff Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 4 Sep 2025 16:42:21 -0700 Subject: [PATCH 02/13] WIP --- Cargo.toml | 2 ++ examples/http_server.rs | 30 +++++++++++++++++------------- macro/src/lib.rs | 19 ++++++++++--------- src/http/body.rs | 40 +++++++++++++++++++++++++++++++++++++++- src/io/cursor.rs | 4 ++++ src/io/empty.rs | 3 +++ src/io/read.rs | 2 ++ src/io/stdio.rs | 3 +++ src/io/streams.rs | 3 +++ src/io/write.rs | 2 ++ src/net/tcp_stream.rs | 6 ++++++ 11 files changed, 91 insertions(+), 23 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fa68f45..47a2b3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ json = ["dep:serde", "dep:serde_json"] [dependencies] async-task.workspace = true +async-trait.workspace = true http.workspace = true itoa.workspace = true pin-project-lite.workspace = true @@ -65,6 +66,7 @@ authors = [ anyhow = "1" async-task = "4.7" cargo_metadata = "0.22" +async-trait = "*" clap = { version = "4.5.26", features = ["derive"] } futures-core = "0.3.19" futures-lite = "1.12.0" diff --git a/examples/http_server.rs b/examples/http_server.rs index b21eda4..7d9cb4d 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -1,30 +1,33 @@ -use wstd::http::body::{BodyForthcoming, IncomingBody, OutgoingBody}; -use wstd::http::server::{Finished, Responder}; -use wstd::http::{IntoBody, Request, Response, StatusCode}; +use anyhow::Result; +use wstd::http::body::{BodyForthcoming, BoxBody, IncomingBody, OutgoingBody}; +use wstd::http::{Body, IntoBody, Request, Response, StatusCode}; use wstd::io::{copy, empty, AsyncWrite}; use wstd::time::{Duration, Instant}; #[wstd::http_server] -async fn main(request: Request, responder: Responder) -> Finished { +async fn main(request: Request) -> Result> { match request.uri().path_and_query().unwrap().as_str() { + /* "/wait" => http_wait(request, responder).await, "/echo" => http_echo(request, responder).await, "/echo-headers" => http_echo_headers(request, responder).await, "/echo-trailers" => http_echo_trailers(request, responder).await, "/fail" => http_fail(request, responder).await, "/bigfail" => http_bigfail(request, responder).await, - "/" => http_home(request, responder).await, - _ => http_not_found(request, responder).await, + */ + "/" => http_home(request).await, + _ => http_not_found(request).await, } } -async fn http_home(_request: Request, responder: Responder) -> Finished { +async fn http_home(_request: Request) -> Result> { // To send a single string as the response body, use `Responder::respond`. - responder - .respond(Response::new("Hello, wasi:http/proxy world!\n".into_body())) - .await + Ok(Response::new( + "Hello, wasi:http/proxy world!\n".into_boxed_body(), + )) } +/* async fn http_wait(_request: Request, responder: Responder) -> Finished { // Get the time now let now = Instant::now(); @@ -84,11 +87,12 @@ async fn http_echo_trailers(request: Request, responder: Responder }; Finished::finish(body, result, trailers) } +*/ -async fn http_not_found(_request: Request, responder: Responder) -> Finished { +async fn http_not_found(_request: Request) -> Result> { let response = Response::builder() .status(StatusCode::NOT_FOUND) - .body(empty()) + .body(empty().into_boxed_body()) .unwrap(); - responder.respond(response).await + Ok(response) } diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 2c6c6e5..b734460 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -92,10 +92,8 @@ pub fn attr_macro_test(_attr: TokenStream, item: TokenStream) -> TokenStream { /// /// ```ignore /// #[wstd::http_server] -/// async fn main(request: Request, responder: Responder) -> Finished { -/// responder -/// .respond(Response::new("Hello!\n".into_body())) -/// .await +/// async fn main(request: Request) -> Result> { +/// Ok(Response::new("Hello!\n".into_body())) /// } /// ``` #[proc_macro_attribute] @@ -137,12 +135,15 @@ pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStr } let responder = ::wstd::http::server::Responder::new(response_out); - let _finished: ::wstd::http::server::Finished = - match ::wstd::http::request::try_from_incoming(request) - { - Ok(request) => ::wstd::runtime::block_on(async { __run(request, responder).await }), + match ::wstd::http::request::try_from_incoming(request) { + Ok(request) => ::wstd::runtime::block_on(async move { + match __run(request).await { + Ok(response) => responder.respond(response), + Err(err) => responder.fail(err), + } + }), Err(err) => responder.fail(err), - }; + } } } diff --git a/src/http/body.rs b/src/http/body.rs index 9eecfae..976ce87 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -38,7 +38,6 @@ impl BodyKind { } /// A trait representing an HTTP body. -#[doc(hidden)] pub trait Body: AsyncRead { /// Returns the exact remaining length of the iterator, if known. fn len(&self) -> Option; @@ -49,6 +48,31 @@ pub trait Body: AsyncRead { } } +/// A boxed trait object for a `Body`. +pub struct BoxBody(pub Box); +impl BoxBody { + fn new(body: impl Body + 'static) -> Self { + BoxBody(Box::new(body)) + } +} +#[async_trait::async_trait(?Send)] +impl AsyncRead for BoxBody { + async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { + self.0.read(buf).await + } +} +impl Body for BoxBody { + /// Returns the exact remaining length of the iterator, if known. + fn len(&self) -> Option { + self.0.len() + } + + /// Returns `true` if the body is known to be empty. + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + /// Conversion into a `Body`. #[doc(hidden)] pub trait IntoBody { @@ -56,6 +80,15 @@ pub trait IntoBody { type IntoBody: Body; /// Convert into `Body`. fn into_body(self) -> Self::IntoBody; + + /// Convert into `BoxBody`. + fn into_boxed_body(self) -> BoxBody + where + Self: Sized, + Self::IntoBody: 'static, + { + BoxBody::new(self.into_body()) + } } impl IntoBody for T where @@ -99,6 +132,7 @@ impl IntoBody for &[u8] { #[derive(Debug)] pub struct BoundedBody(Cursor); +#[async_trait::async_trait(?Send)] impl> AsyncRead for BoundedBody { async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { self.0.read(buf).await @@ -120,6 +154,8 @@ impl StreamedBody { Self(s) } } + +#[async_trait::async_trait(?Send)] impl AsyncRead for StreamedBody { async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { self.0.read(buf).await @@ -214,6 +250,7 @@ impl IncomingBody { } } +#[async_trait::async_trait(?Send)] impl AsyncRead for IncomingBody { async fn read(&mut self, out_buf: &mut [u8]) -> crate::io::Result { self.body_stream.read(out_buf).await @@ -309,6 +346,7 @@ impl OutgoingBody { } } +#[async_trait::async_trait(?Send)] impl AsyncWrite for OutgoingBody { async fn write(&mut self, buf: &[u8]) -> crate::io::Result { self.stream.write(buf).await diff --git a/src/io/cursor.rs b/src/io/cursor.rs index f05c284..51e4f7b 100644 --- a/src/io/cursor.rs +++ b/src/io/cursor.rs @@ -57,6 +57,7 @@ where } } +#[async_trait::async_trait(?Send)] impl AsyncRead for Cursor where T: AsRef<[u8]>, @@ -66,6 +67,7 @@ where } } +#[async_trait::async_trait(?Send)] impl AsyncWrite for Cursor<&mut [u8]> { async fn write(&mut self, buf: &[u8]) -> io::Result { std::io::Write::write(&mut self.inner, buf) @@ -75,6 +77,7 @@ impl AsyncWrite for Cursor<&mut [u8]> { } } +#[async_trait::async_trait(?Send)] impl AsyncWrite for Cursor<&mut Vec> { async fn write(&mut self, buf: &[u8]) -> io::Result { std::io::Write::write(&mut self.inner, buf) @@ -84,6 +87,7 @@ impl AsyncWrite for Cursor<&mut Vec> { } } +#[async_trait::async_trait(?Send)] impl AsyncWrite for Cursor> { async fn write(&mut self, buf: &[u8]) -> io::Result { std::io::Write::write(&mut self.inner, buf) diff --git a/src/io/empty.rs b/src/io/empty.rs index 9fbf873..386160c 100644 --- a/src/io/empty.rs +++ b/src/io/empty.rs @@ -2,12 +2,15 @@ use super::{AsyncRead, AsyncWrite}; #[non_exhaustive] pub struct Empty; + +#[async_trait::async_trait(?Send)] impl AsyncRead for Empty { async fn read(&mut self, _buf: &mut [u8]) -> super::Result { Ok(0) } } +#[async_trait::async_trait(?Send)] impl AsyncWrite for Empty { async fn write(&mut self, buf: &[u8]) -> super::Result { Ok(buf.len()) diff --git a/src/io/read.rs b/src/io/read.rs index a6a95da..a3b8ad4 100644 --- a/src/io/read.rs +++ b/src/io/read.rs @@ -2,6 +2,7 @@ use crate::io; const CHUNK_SIZE: usize = 2048; +#[async_trait::async_trait(?Send)] /// Read bytes from a source. pub trait AsyncRead { async fn read(&mut self, buf: &mut [u8]) -> io::Result; @@ -33,6 +34,7 @@ pub trait AsyncRead { } } +#[async_trait::async_trait(?Send)] impl AsyncRead for &mut R { #[inline] async fn read(&mut self, buf: &mut [u8]) -> io::Result { diff --git a/src/io/stdio.rs b/src/io/stdio.rs index 0a37e2a..0b9707d 100644 --- a/src/io/stdio.rs +++ b/src/io/stdio.rs @@ -26,6 +26,7 @@ impl Stdin { } } +#[async_trait::async_trait(?Send)] impl AsyncRead for Stdin { #[inline] async fn read(&mut self, buf: &mut [u8]) -> Result { @@ -66,6 +67,7 @@ impl Stdout { } } +#[async_trait::async_trait(?Send)] impl AsyncWrite for Stdout { #[inline] async fn write(&mut self, buf: &[u8]) -> Result { @@ -111,6 +113,7 @@ impl Stderr { } } +#[async_trait::async_trait(?Send)] impl AsyncWrite for Stderr { #[inline] async fn write(&mut self, buf: &[u8]) -> Result { diff --git a/src/io/streams.rs b/src/io/streams.rs index d8b35ec..2e5bfe5 100644 --- a/src/io/streams.rs +++ b/src/io/streams.rs @@ -58,6 +58,7 @@ impl AsyncInputStream { } } +#[async_trait::async_trait(?Send)] impl AsyncRead for AsyncInputStream { async fn read(&mut self, buf: &mut [u8]) -> Result { Self::read(self, buf).await @@ -160,6 +161,8 @@ impl AsyncOutputStream { } } } + +#[async_trait::async_trait(?Send)] impl AsyncWrite for AsyncOutputStream { // Required methods async fn write(&mut self, buf: &[u8]) -> Result { diff --git a/src/io/write.rs b/src/io/write.rs index 79cf0d9..ce45121 100644 --- a/src/io/write.rs +++ b/src/io/write.rs @@ -1,6 +1,7 @@ use crate::io; /// Write bytes to a sink. +#[async_trait::async_trait(?Send)] pub trait AsyncWrite { // Required methods async fn write(&mut self, buf: &[u8]) -> io::Result; @@ -25,6 +26,7 @@ pub trait AsyncWrite { } } +#[async_trait::async_trait(?Send)] impl AsyncWrite for &mut W { #[inline] async fn write(&mut self, buf: &[u8]) -> io::Result { diff --git a/src/net/tcp_stream.rs b/src/net/tcp_stream.rs index fc6ef99..6bee699 100644 --- a/src/net/tcp_stream.rs +++ b/src/net/tcp_stream.rs @@ -42,6 +42,7 @@ impl Drop for TcpStream { } } +#[async_trait::async_trait(?Send)] impl io::AsyncRead for TcpStream { async fn read(&mut self, buf: &mut [u8]) -> io::Result { self.input.read(buf).await @@ -52,6 +53,7 @@ impl io::AsyncRead for TcpStream { } } +#[async_trait::async_trait(?Send)] impl io::AsyncRead for &TcpStream { async fn read(&mut self, buf: &mut [u8]) -> io::Result { self.input.read(buf).await @@ -62,6 +64,7 @@ impl io::AsyncRead for &TcpStream { } } +#[async_trait::async_trait(?Send)] impl io::AsyncWrite for TcpStream { async fn write(&mut self, buf: &[u8]) -> io::Result { self.output.write(buf).await @@ -76,6 +79,7 @@ impl io::AsyncWrite for TcpStream { } } +#[async_trait::async_trait(?Send)] impl io::AsyncWrite for &TcpStream { async fn write(&mut self, buf: &[u8]) -> io::Result { self.output.write(buf).await @@ -91,6 +95,7 @@ impl io::AsyncWrite for &TcpStream { } pub struct ReadHalf<'a>(&'a TcpStream); +#[async_trait::async_trait(?Send)] impl<'a> io::AsyncRead for ReadHalf<'a> { async fn read(&mut self, buf: &mut [u8]) -> io::Result { self.0.read(buf).await @@ -111,6 +116,7 @@ impl<'a> Drop for ReadHalf<'a> { } pub struct WriteHalf<'a>(&'a TcpStream); +#[async_trait::async_trait(?Send)] impl<'a> io::AsyncWrite for WriteHalf<'a> { async fn write(&mut self, buf: &[u8]) -> io::Result { self.0.write(buf).await From eaf2d29195283d1401c95c61e5cf50925452dfe3 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Thu, 4 Sep 2025 18:07:47 -0700 Subject: [PATCH 03/13] further WIP --- examples/http_server.rs | 8 ++++++-- macro/src/lib.rs | 14 +++++++------- src/http/error.rs | 10 +++++----- src/http/request.rs | 18 ++++++++---------- src/http/server.rs | 15 ++++++++------- test-programs/Cargo.toml | 1 + test-programs/artifacts/tests/http_server.rs | 4 ++-- 7 files changed, 37 insertions(+), 33 deletions(-) diff --git a/examples/http_server.rs b/examples/http_server.rs index 7d9cb4d..27c403f 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -1,11 +1,11 @@ use anyhow::Result; use wstd::http::body::{BodyForthcoming, BoxBody, IncomingBody, OutgoingBody}; -use wstd::http::{Body, IntoBody, Request, Response, StatusCode}; +use wstd::http::{error::ErrorCode, Body, IntoBody, Request, Response, StatusCode}; use wstd::io::{copy, empty, AsyncWrite}; use wstd::time::{Duration, Instant}; #[wstd::http_server] -async fn main(request: Request) -> Result> { +async fn main(request: Request) -> Result, ErrorCode> { match request.uri().path_and_query().unwrap().as_str() { /* "/wait" => http_wait(request, responder).await, @@ -18,6 +18,10 @@ async fn main(request: Request) -> Result> { "/" => http_home(request).await, _ => http_not_found(request).await, } + .map_err(|e| match e.downcast::() { + Ok(e) => e, + Err(e) => ErrorCode::InternalError(Some(format!("{e:?}"))), + }) } async fn http_home(_request: Request) -> Result> { diff --git a/macro/src/lib.rs b/macro/src/lib.rs index b734460..b13cff0 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -135,15 +135,15 @@ pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStr } let responder = ::wstd::http::server::Responder::new(response_out); - match ::wstd::http::request::try_from_incoming(request) { - Ok(request) => ::wstd::runtime::block_on(async move { - match __run(request).await { - Ok(response) => responder.respond(response), + ::wstd::runtime::block_on(async move { + match ::wstd::http::request::try_from_incoming(request) { + Ok(request) => match __run(request).await { + Ok(response) => responder.respond(response).await, Err(err) => responder.fail(err), } - }), - Err(err) => responder.fail(err), - } + Err(err) => responder.fail(err), + } + }) } } diff --git a/src/http/error.rs b/src/http/error.rs index c3c540b..2eb3922 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -12,7 +12,7 @@ pub struct Error { pub use http::header::{InvalidHeaderName, InvalidHeaderValue}; pub use http::method::InvalidMethod; -pub use wasip2::http::types::{ErrorCode as WasiHttpErrorCode, HeaderError as WasiHttpHeaderError}; +pub use wasip2::http::types::{ErrorCode, HeaderError}; impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -73,8 +73,8 @@ impl From for Error { } } -impl From for Error { - fn from(e: WasiHttpErrorCode) -> Error { +impl From for Error { + fn from(e: ErrorCode) -> Error { ErrorVariant::WasiHttp(e).into() } } @@ -114,8 +114,8 @@ impl From for Error { #[derive(Debug)] pub enum ErrorVariant { - WasiHttp(WasiHttpErrorCode), - WasiHeader(WasiHttpHeaderError), + WasiHttp(ErrorCode), + WasiHeader(HeaderError), HeaderName(InvalidHeaderName), HeaderValue(InvalidHeaderValue), Method(InvalidMethod), diff --git a/src/http/request.rs b/src/http/request.rs index d150367..e307abc 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,6 +1,6 @@ use super::{ body::{BodyKind, IncomingBody}, - error::WasiHttpErrorCode, + error::ErrorCode, fields::{header_map_from_wasi, header_map_to_wasi}, method::{from_wasi_method, to_wasi_method}, scheme::{from_wasi_scheme, to_wasi_scheme}, @@ -97,15 +97,13 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque /// This is used by the `http_server` macro. #[doc(hidden)] -pub fn try_from_incoming( - incoming: IncomingRequest, -) -> Result, WasiHttpErrorCode> { +pub fn try_from_incoming(incoming: IncomingRequest) -> Result, ErrorCode> { // TODO: What's the right error code to use for invalid headers? let headers: HeaderMap = header_map_from_wasi(incoming.headers()) - .map_err(|e| WasiHttpErrorCode::InternalError(Some(e.to_string())))?; + .map_err(|e| ErrorCode::InternalError(Some(e.to_string())))?; - let method = from_wasi_method(incoming.method()) - .map_err(|_| WasiHttpErrorCode::HttpRequestMethodInvalid)?; + let method = + from_wasi_method(incoming.method()).map_err(|_| ErrorCode::HttpRequestMethodInvalid)?; let scheme = incoming.scheme().map(|scheme| { from_wasi_scheme(scheme).expect("TODO: what shall we do with an invalid uri here?") }); @@ -120,7 +118,7 @@ pub fn try_from_incoming( // TODO: What's the right error code to use for invalid headers? let kind = BodyKind::from_headers(&headers) - .map_err(|e| WasiHttpErrorCode::InternalError(Some(e.to_string())))?; + .map_err(|e| ErrorCode::InternalError(Some(e.to_string())))?; // `body_stream` is a child of `incoming_body` which means we cannot // drop the parent before we drop the child let incoming_body = incoming @@ -146,7 +144,7 @@ pub fn try_from_incoming( // TODO: What's the right error code to use for an invalid uri? let uri = uri .build() - .map_err(|e| WasiHttpErrorCode::InternalError(Some(e.to_string())))?; + .map_err(|e| ErrorCode::InternalError(Some(e.to_string())))?; let mut request = Request::builder().method(method).uri(uri); if let Some(headers_mut) = request.headers_mut() { @@ -155,5 +153,5 @@ pub fn try_from_incoming( // TODO: What's the right error code to use for an invalid request? request .body(body) - .map_err(|e| WasiHttpErrorCode::InternalError(Some(e.to_string()))) + .map_err(|e| ErrorCode::InternalError(Some(e.to_string()))) } diff --git a/src/http/server.rs b/src/http/server.rs index 7a9117d..d085fa2 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -22,7 +22,7 @@ use super::{ body::{BodyForthcoming, OutgoingBody}, - error::WasiHttpErrorCode, + error::ErrorCode, fields::header_map_to_wasi, Body, HeaderMap, Response, }; @@ -31,6 +31,7 @@ use http::header::CONTENT_LENGTH; use wasip2::exports::http::incoming_handler::ResponseOutparam; use wasip2::http::types::OutgoingResponse; +/// TK REWRITE THESE DOCS /// This is passed into the [`http_server`] `main` function and holds the state /// needed for a handler to produce a response, or fail. There are two ways to /// respond, with [`Responder::start_response`] to stream the body in, or @@ -92,14 +93,14 @@ impl Responder { /// # use wstd::http::{body::{IncomingBody, BodyForthcoming}, IntoBody, Response, Request}; /// # use wstd::http::server::{Finished, Responder}; /// # - /// # async fn example(responder: Responder) -> Finished { + /// # async fn example(responder: Responder) { /// responder /// .respond(Response::new("Hello!\n".into_body())) /// .await /// # } /// # fn main() {} /// ``` - pub async fn respond(self, response: Response) -> Finished { + pub async fn respond(self, response: Response) { let headers = response.headers(); let status = response.status().as_u16(); @@ -128,11 +129,12 @@ impl Responder { // Tell WASI to start the show. ResponseOutparam::set(self.outparam, Ok(wasi_response)); + // TODO: Move this out to BodyForthcoming! let mut outgoing_body = OutgoingBody::new(AsyncOutputStream::new(wasi_stream), wasi_body); let result = copy(&mut body, &mut outgoing_body).await; let trailers = None; - Finished::finish(outgoing_body, result, trailers) + Finished::finish(outgoing_body, result, trailers); } /// This is used by the `http_server` macro. @@ -143,9 +145,8 @@ impl Responder { /// This is used by the `http_server` macro. #[doc(hidden)] - pub fn fail(self, err: WasiHttpErrorCode) -> Finished { - ResponseOutparam::set(self.outparam, Err(err)); - Finished(()) + pub fn fail(self, err: ErrorCode) { + ResponseOutparam::set(self.outparam, Err(err)) } } diff --git a/test-programs/Cargo.toml b/test-programs/Cargo.toml index 84610da..d9b97e0 100644 --- a/test-programs/Cargo.toml +++ b/test-programs/Cargo.toml @@ -7,6 +7,7 @@ edition.workspace = true rust-version.workspace = true [dependencies] +anyhow.workspace = true futures-lite.workspace = true serde_json.workspace = true wstd.workspace = true diff --git a/test-programs/artifacts/tests/http_server.rs b/test-programs/artifacts/tests/http_server.rs index 995298b..7823550 100644 --- a/test-programs/artifacts/tests/http_server.rs +++ b/test-programs/artifacts/tests/http_server.rs @@ -32,11 +32,11 @@ fn http_server() -> Result<()> { match ureq::get("http://127.0.0.1:8081/fail").call() { Ok(body) => { - unreachable!("unexpected success from /fail: {:?}", body); + panic!("unexpected success from /fail: {:?}", body); } Err(ureq::Error::Transport(_transport)) => {} Err(other) => { - unreachable!("unexpected error: {:?}", other); + panic!("unexpected error: {:?}", other); } } From 566fa0b0d055fee4e1f800bbf3d73b032b6d296f Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 10 Sep 2025 10:11:31 -0700 Subject: [PATCH 04/13] idk man this is a mess --- Cargo.toml | 8 +- src/http/body.rs | 434 ++++++++------------------------------ src/http/body.rs.disabled | 391 ++++++++++++++++++++++++++++++++++ src/http/client.rs | 122 +---------- src/http/error.rs | 6 + src/http/mod.rs | 2 +- src/http/request.rs | 57 +---- src/http/response.rs | 15 +- src/http/server.rs | 120 +---------- 9 files changed, 519 insertions(+), 636 deletions(-) create mode 100644 src/http/body.rs.disabled diff --git a/Cargo.toml b/Cargo.toml index 47a2b3c..caafa4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,10 @@ json = ["dep:serde", "dep:serde_json"] [dependencies] async-task.workspace = true async-trait.workspace = true +bytes.workspace = true http.workspace = true +http-body.workspace = true +http-body-util.workspace = true itoa.workspace = true pin-project-lite.workspace = true slab.workspace = true @@ -65,8 +68,9 @@ authors = [ [workspace.dependencies] anyhow = "1" async-task = "4.7" -cargo_metadata = "0.22" async-trait = "*" +bytes = "1.10.1" +cargo_metadata = "0.22" clap = { version = "4.5.26", features = ["derive"] } futures-core = "0.3.19" futures-lite = "1.12.0" @@ -74,6 +78,8 @@ futures-concurrency = "7.6" humantime = "2.1.0" heck = "0.5" http = "1.1" +http-body = "1.0.1" +http-body-util = "0.1.3" itoa = "1" pin-project-lite = "0.2.8" quote = "1.0" diff --git a/src/http/body.rs b/src/http/body.rs index 976ce87..6b35b04 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -1,391 +1,131 @@ -//! HTTP body types +use crate::http::HeaderMap; +use crate::io::AsyncInputStream; +use crate::runtime::{AsyncPollable, WaitFor}; -use crate::http::fields::header_map_from_wasi; -use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Cursor, Empty}; -use crate::runtime::AsyncPollable; -use core::fmt; -use http::header::CONTENT_LENGTH; -use wasip2::http::types::IncomingBody as WasiIncomingBody; - -#[cfg(feature = "json")] -use serde::de::DeserializeOwned; -#[cfg(feature = "json")] -use serde_json; - -pub use super::{ - error::{Error, ErrorVariant}, - HeaderMap, -}; - -#[derive(Debug)] -pub(crate) enum BodyKind { - Fixed(u64), - Chunked, -} - -impl BodyKind { - pub(crate) fn from_headers(headers: &HeaderMap) -> Result { - if let Some(value) = headers.get(CONTENT_LENGTH) { - let content_length = std::str::from_utf8(value.as_ref()) - .unwrap() - .parse::() - .map_err(|_| InvalidContentLength)?; - Ok(BodyKind::Fixed(content_length)) - } else { - Ok(BodyKind::Chunked) - } - } -} - -/// A trait representing an HTTP body. -pub trait Body: AsyncRead { - /// Returns the exact remaining length of the iterator, if known. - fn len(&self) -> Option; +pub use ::http_body::{Body as HttpBody, Frame, SizeHint}; +pub use bytes::Bytes; - /// Returns `true` if the body is known to be empty. - fn is_empty(&self) -> bool { - matches!(self.len(), Some(0)) - } -} +use http::header::CONTENT_LENGTH; +use http_body_util::combinators::BoxBody; +use std::fmt; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use wasip2::http::types::{ErrorCode, Fields, FutureTrailers, IncomingBody as WasiIncomingBody}; +use wasip2::io::streams::StreamError; -/// A boxed trait object for a `Body`. -pub struct BoxBody(pub Box); -impl BoxBody { - fn new(body: impl Body + 'static) -> Self { - BoxBody(Box::new(body)) - } +pub mod util { + pub use http_body_util::*; } -#[async_trait::async_trait(?Send)] -impl AsyncRead for BoxBody { - async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { - self.0.read(buf).await - } -} -impl Body for BoxBody { - /// Returns the exact remaining length of the iterator, if known. - fn len(&self) -> Option { - self.0.len() - } - /// Returns `true` if the body is known to be empty. - fn is_empty(&self) -> bool { - self.0.is_empty() - } +pub struct Body(pub(crate) BodyInner); +pub(crate) enum BodyInner { + Boxed(BoxBody>), + Incoming(Incoming), } -/// Conversion into a `Body`. -#[doc(hidden)] -pub trait IntoBody { - /// What type of `Body` are we turning this into? - type IntoBody: Body; - /// Convert into `Body`. - fn into_body(self) -> Self::IntoBody; - - /// Convert into `BoxBody`. - fn into_boxed_body(self) -> BoxBody - where - Self: Sized, - Self::IntoBody: 'static, - { - BoxBody::new(self.into_body()) - } -} -impl IntoBody for T +impl From for Body where - T: Body, + B: HttpBody + Send + Sync + 'static, + ::Data: Into, + ::Error: Into>, { - type IntoBody = T; - fn into_body(self) -> Self::IntoBody { - self - } -} - -impl IntoBody for String { - type IntoBody = BoundedBody>; - fn into_body(self) -> Self::IntoBody { - BoundedBody(Cursor::new(self.into_bytes())) - } -} - -impl IntoBody for &str { - type IntoBody = BoundedBody>; - fn into_body(self) -> Self::IntoBody { - BoundedBody(Cursor::new(self.to_owned().into_bytes())) - } -} - -impl IntoBody for Vec { - type IntoBody = BoundedBody>; - fn into_body(self) -> Self::IntoBody { - BoundedBody(Cursor::new(self)) + fn from(http_body: B) -> Body { + use util::BodyExt; + Body(BodyInner::Boxed( + http_body + .map_frame(|f| f.map_data(Into::into)) + .map_err(Into::into) + .boxed(), + )) } } -impl IntoBody for &[u8] { - type IntoBody = BoundedBody>; - fn into_body(self) -> Self::IntoBody { - BoundedBody(Cursor::new(self.to_owned())) +impl From for Body { + fn from(incoming: Incoming) -> Body { + Body(BodyInner::Incoming(incoming)) } } -/// An HTTP body with a known length -#[derive(Debug)] -pub struct BoundedBody(Cursor); - -#[async_trait::async_trait(?Send)] -impl> AsyncRead for BoundedBody { - async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { - self.0.read(buf).await - } -} -impl> Body for BoundedBody { - fn len(&self) -> Option { - Some(self.0.get_ref().as_ref().len()) +impl Body { + pub(crate) fn content_length(&self) -> Option { + match &self.0 { + BodyInner::Boxed(b) => b.size_hint().exact(), + BodyInner::Incoming(i) => i.size_hint.content_length(), + } } } -/// An HTTP body with an unknown length -#[derive(Debug)] -pub struct StreamedBody(S); - -impl StreamedBody { - /// Wrap an `AsyncRead` impl in a type that provides a [`Body`] implementation. - pub fn new(s: S) -> Self { - Self(s) - } +pub struct Incoming { + body: WasiIncomingBody, + size_hint: BodyHint, } -#[async_trait::async_trait(?Send)] -impl AsyncRead for StreamedBody { - async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { - self.0.read(buf).await +impl Incoming { + pub(crate) fn new(body: WasiIncomingBody, size_hint: BodyHint) -> Self { + Self { body, size_hint } } -} -impl Body for StreamedBody { - fn len(&self) -> Option { - None + /// Use with `http_body::Body` trait + pub fn into_http_body(self) -> IncomingBody { + IncomingBody::new(self) } -} - -impl Body for Empty { - fn len(&self) -> Option { - Some(0) + pub fn into_input_stream(self) -> AsyncInputStream { + AsyncInputStream::new( + self.body + .stream() + .expect("incoming body stream has not been taken"), + ) } } -/// An incoming HTTP body -#[derive(Debug)] -pub struct IncomingBody { - kind: BodyKind, - // IMPORTANT: the order of these fields here matters. `body_stream` must - // be dropped before `incoming_body`. - body_stream: AsyncInputStream, - incoming_body: WasiIncomingBody, +#[derive(Clone, Copy, Debug)] +pub enum BodyHint { + ContentLength(u64), + Unknown, } -impl IncomingBody { - pub(crate) fn new( - kind: BodyKind, - body_stream: AsyncInputStream, - incoming_body: WasiIncomingBody, - ) -> Self { - Self { - kind, - body_stream, - incoming_body, +impl BodyHint { + pub fn from_headers(headers: &HeaderMap) -> Result { + if let Some(val) = headers.get(CONTENT_LENGTH) { + let len = std::str::from_utf8(val.as_ref()) + .map_err(|_| InvalidContentLength)? + .parse::() + .map_err(|_| InvalidContentLength)?; + Ok(BodyHint::ContentLength(len)) + } else { + Ok(BodyHint::Unknown) } } - - /// Consume this `IncomingBody` and return the trailers, if present. - pub async fn finish(self) -> Result, Error> { - // The stream is a child resource of the `IncomingBody`, so ensure that - // it's dropped first. - drop(self.body_stream); - - let trailers = WasiIncomingBody::finish(self.incoming_body); - - AsyncPollable::new(trailers.subscribe()).wait_for().await; - - let trailers = trailers.get().unwrap().unwrap()?; - - let trailers = match trailers { - None => None, - Some(trailers) => Some(header_map_from_wasi(trailers)?), - }; - - Ok(trailers) - } - - /// Try to deserialize the incoming body as JSON. The optional - /// `json` feature is required. - /// - /// Fails whenever the response body is not in JSON format, - /// or it cannot be properly deserialized to target type `T`. For more - /// details please see [`serde_json::from_reader`]. - /// - /// [`serde_json::from_reader`]: https://docs.serde.rs/serde_json/fn.from_reader.html - #[cfg(feature = "json")] - pub async fn json(&mut self) -> Result { - let buf = self.bytes().await?; - serde_json::from_slice(&buf).map_err(|e| ErrorVariant::Other(e.to_string()).into()) - } - - /// Get the full response body as `Vec`. - pub async fn bytes(&mut self) -> Result, Error> { - let mut buf = match self.kind { - BodyKind::Fixed(l) => { - if l > (usize::MAX as u64) { - return Err(ErrorVariant::Other( - "incoming body is too large to allocate and buffer in memory".to_string(), - ) - .into()); - } else { - Vec::with_capacity(l as usize) - } - } - BodyKind::Chunked => Vec::with_capacity(4096), - }; - self.read_to_end(&mut buf).await?; - Ok(buf) - } -} - -#[async_trait::async_trait(?Send)] -impl AsyncRead for IncomingBody { - async fn read(&mut self, out_buf: &mut [u8]) -> crate::io::Result { - self.body_stream.read(out_buf).await - } - - fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { - Some(&self.body_stream) - } -} - -impl Body for IncomingBody { - fn len(&self) -> Option { - match self.kind { - BodyKind::Fixed(l) => { - if l > (usize::MAX as u64) { - None - } else { - Some(l as usize) - } - } - BodyKind::Chunked => None, + fn content_length(&self) -> Option { + match self { + BodyHint::ContentLength(l) => Some(*l), + _ => None, } } } - #[derive(Debug)] pub struct InvalidContentLength; - impl fmt::Display for InvalidContentLength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - "incoming content-length should be a u64; violates HTTP/1.1".fmt(f) + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Invalid Content-Length header") } } - impl std::error::Error for InvalidContentLength {} -impl From for Error { - fn from(e: InvalidContentLength) -> Self { - // TODO: What's the right error code here? - ErrorVariant::Other(e.to_string()).into() - } -} - -/// The output stream for the body, implementing [`AsyncWrite`]. Call -/// [`Responder::start_response`] or [`Client::start_request`] to obtain -/// one. Once the body is complete, it must be declared finished, using -/// [`Finished::finish`], [`Finished::fail`], [`Client::finish`], or -/// [`Client::fail`]. -/// -/// [`Responder::start_response`]: crate::http::server::Responder::start_response -/// [`Client::start_request`]: crate::http::client::Client::start_request -/// [`Finished::finish`]: crate::http::server::Finished::finish -/// [`Finished::fail`]: crate::http::server::Finished::fail -/// [`Client::finish`]: crate::http::client::Client::finish -/// [`Client::fail`]: crate::http::client::Client::fail -#[must_use] -pub struct OutgoingBody { - // IMPORTANT: the order of these fields here matters. `stream` must - // be dropped before `body`. - stream: AsyncOutputStream, - body: wasip2::http::types::OutgoingBody, - dontdrop: DontDropOutgoingBody, -} - -impl OutgoingBody { - pub(crate) fn new(stream: AsyncOutputStream, body: wasip2::http::types::OutgoingBody) -> Self { - Self { - stream, - body, - dontdrop: DontDropOutgoingBody, - } - } - - pub(crate) fn consume(self) -> (AsyncOutputStream, wasip2::http::types::OutgoingBody) { - let Self { - stream, - body, - dontdrop, - } = self; - - std::mem::forget(dontdrop); - - (stream, body) - } - - /// Return a reference to the underlying `AsyncOutputStream`. - /// - /// This usually isn't needed, as `OutgoingBody` implements `AsyncWrite` - /// too, however it is useful for code that expects to work with - /// `AsyncOutputStream` specifically. - pub fn stream(&mut self) -> &mut AsyncOutputStream { - &mut self.stream - } +pub struct IncomingBody { + stream: Option, + body: Option, + size_hint: BodyHint, } -#[async_trait::async_trait(?Send)] -impl AsyncWrite for OutgoingBody { - async fn write(&mut self, buf: &[u8]) -> crate::io::Result { - self.stream.write(buf).await - } - - async fn flush(&mut self) -> crate::io::Result<()> { - self.stream.flush().await - } - - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { - Some(&self.stream) - } +enum IncomingBodyState { + Body(WasiIncomingBody), + FutureTrailers(FutureTrailers), + Trailers(Result, ErrorCode>), } -/// A utility to ensure that `OutgoingBody` is either finished or failed, and -/// not implicitly dropped. -struct DontDropOutgoingBody; - -impl Drop for DontDropOutgoingBody { - fn drop(&mut self) { - unreachable!("`OutgoingBody::drop` called; `OutgoingBody`s should be consumed with `finish` or `fail`."); - } +struct ReadState { + read_op: Option, StreamError>>>>>, + stream: AsyncInputStream, + body: WasiIncomingBody, } - -/// A placeholder for use as the type parameter to [`Request`] and [`Response`] -/// to indicate that the body has not yet started. This is used with -/// [`Client::start_request`] and [`Responder::start_response`], which have -/// `Requeset` and `Response` arguments, -/// respectively. -/// -/// To instead start the response and obtain the output stream for the body, -/// use [`Responder::respond`]. -/// To instead send a request or response with an input stream for the body, -/// use [`Client::send`] or [`Responder::respond`]. -/// -/// [`Request`]: crate::http::Request -/// [`Response`]: crate::http::Response -/// [`Client::start_request`]: crate::http::Client::start_request -/// [`Responder::start_response`]: crate::http::server::Responder::start_response -/// [`Client::send`]: crate::http::Client::send -/// [`Responder::respond`]: crate::http::server::Responder::respond -pub struct BodyForthcoming; diff --git a/src/http/body.rs.disabled b/src/http/body.rs.disabled new file mode 100644 index 0000000..bd6f65b --- /dev/null +++ b/src/http/body.rs.disabled @@ -0,0 +1,391 @@ +//! HTTP body types + +use crate::http::fields::header_map_from_wasi; +use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Cursor, Empty}; +use crate::runtime::AsyncPollable; +use core::fmt; +use http::header::CONTENT_LENGTH; +use wasi::http::types::IncomingBody as WasiIncomingBody; + +#[cfg(feature = "json")] +use serde::de::DeserializeOwned; +#[cfg(feature = "json")] +use serde_json; + +pub use super::{ + error::{Error, ErrorVariant}, + HeaderMap, +}; + +#[derive(Debug)] +pub(crate) enum BodyKind { + Fixed(u64), + Chunked, +} + +impl BodyKind { + pub(crate) fn from_headers(headers: &HeaderMap) -> Result { + if let Some(value) = headers.get(CONTENT_LENGTH) { + let content_length = std::str::from_utf8(value.as_ref()) + .unwrap() + .parse::() + .map_err(|_| InvalidContentLength)?; + Ok(BodyKind::Fixed(content_length)) + } else { + Ok(BodyKind::Chunked) + } + } +} + +/// A trait representing an HTTP body. +pub trait Body: AsyncRead { + /// Returns the exact remaining length of the iterator, if known. + fn len(&self) -> Option; + + /// Returns `true` if the body is known to be empty. + fn is_empty(&self) -> bool { + matches!(self.len(), Some(0)) + } +} + +/// A boxed trait object for a `Body`. +pub struct BoxBody(pub Box); +impl BoxBody { + fn new(body: impl Body + 'static) -> Self { + BoxBody(Box::new(body)) + } +} +#[async_trait::async_trait(?Send)] +impl AsyncRead for BoxBody { + async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { + self.0.read(buf).await + } +} +impl Body for BoxBody { + /// Returns the exact remaining length of the iterator, if known. + fn len(&self) -> Option { + self.0.len() + } + + /// Returns `true` if the body is known to be empty. + fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Conversion into a `Body`. +#[doc(hidden)] +pub trait IntoBody { + /// What type of `Body` are we turning this into? + type IntoBody: Body; + /// Convert into `Body`. + fn into_body(self) -> Self::IntoBody; + + /// Convert into `BoxBody`. + fn into_boxed_body(self) -> BoxBody + where + Self: Sized, + Self::IntoBody: 'static, + { + BoxBody::new(self.into_body()) + } +} +impl IntoBody for T +where + T: Body, +{ + type IntoBody = T; + fn into_body(self) -> Self::IntoBody { + self + } +} + +impl IntoBody for String { + type IntoBody = BoundedBody>; + fn into_body(self) -> Self::IntoBody { + BoundedBody(Cursor::new(self.into_bytes())) + } +} + +impl IntoBody for &str { + type IntoBody = BoundedBody>; + fn into_body(self) -> Self::IntoBody { + BoundedBody(Cursor::new(self.to_owned().into_bytes())) + } +} + +impl IntoBody for Vec { + type IntoBody = BoundedBody>; + fn into_body(self) -> Self::IntoBody { + BoundedBody(Cursor::new(self)) + } +} + +impl IntoBody for &[u8] { + type IntoBody = BoundedBody>; + fn into_body(self) -> Self::IntoBody { + BoundedBody(Cursor::new(self.to_owned())) + } +} + +/// An HTTP body with a known length +#[derive(Debug)] +pub struct BoundedBody(Cursor); + +#[async_trait::async_trait(?Send)] +impl> AsyncRead for BoundedBody { + async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { + self.0.read(buf).await + } +} +impl> Body for BoundedBody { + fn len(&self) -> Option { + Some(self.0.get_ref().as_ref().len()) + } +} + +/// An HTTP body with an unknown length +#[derive(Debug)] +pub struct StreamedBody(S); + +impl StreamedBody { + /// Wrap an `AsyncRead` impl in a type that provides a [`Body`] implementation. + pub fn new(s: S) -> Self { + Self(s) + } +} + +#[async_trait::async_trait(?Send)] +impl AsyncRead for StreamedBody { + async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { + self.0.read(buf).await + } +} +impl Body for StreamedBody { + fn len(&self) -> Option { + None + } +} + +impl Body for Empty { + fn len(&self) -> Option { + Some(0) + } +} + +/// An incoming HTTP body +#[derive(Debug)] +pub struct IncomingBody { + kind: BodyKind, + // IMPORTANT: the order of these fields here matters. `body_stream` must + // be dropped before `incoming_body`. + body_stream: AsyncInputStream, + incoming_body: WasiIncomingBody, +} + +impl IncomingBody { + pub(crate) fn new( + kind: BodyKind, + body_stream: AsyncInputStream, + incoming_body: WasiIncomingBody, + ) -> Self { + Self { + kind, + body_stream, + incoming_body, + } + } + + /// Consume this `IncomingBody` and return the trailers, if present. + pub async fn finish(self) -> Result, Error> { + // The stream is a child resource of the `IncomingBody`, so ensure that + // it's dropped first. + drop(self.body_stream); + + let trailers = WasiIncomingBody::finish(self.incoming_body); + + AsyncPollable::new(trailers.subscribe()).wait_for().await; + + let trailers = trailers.get().unwrap().unwrap()?; + + let trailers = match trailers { + None => None, + Some(trailers) => Some(header_map_from_wasi(trailers)?), + }; + + Ok(trailers) + } + + /// Try to deserialize the incoming body as JSON. The optional + /// `json` feature is required. + /// + /// Fails whenever the response body is not in JSON format, + /// or it cannot be properly deserialized to target type `T`. For more + /// details please see [`serde_json::from_reader`]. + /// + /// [`serde_json::from_reader`]: https://docs.serde.rs/serde_json/fn.from_reader.html + #[cfg(feature = "json")] + pub async fn json(&mut self) -> Result { + let buf = self.bytes().await?; + serde_json::from_slice(&buf).map_err(|e| ErrorVariant::Other(e.to_string()).into()) + } + + /// Get the full response body as `Vec`. + pub async fn bytes(&mut self) -> Result, Error> { + let mut buf = match self.kind { + BodyKind::Fixed(l) => { + if l > (usize::MAX as u64) { + return Err(ErrorVariant::Other( + "incoming body is too large to allocate and buffer in memory".to_string(), + ) + .into()); + } else { + Vec::with_capacity(l as usize) + } + } + BodyKind::Chunked => Vec::with_capacity(4096), + }; + self.read_to_end(&mut buf).await?; + Ok(buf) + } +} + +#[async_trait::async_trait(?Send)] +impl AsyncRead for IncomingBody { + async fn read(&mut self, out_buf: &mut [u8]) -> crate::io::Result { + self.body_stream.read(out_buf).await + } + + fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { + Some(&self.body_stream) + } +} + +impl Body for IncomingBody { + fn len(&self) -> Option { + match self.kind { + BodyKind::Fixed(l) => { + if l > (usize::MAX as u64) { + None + } else { + Some(l as usize) + } + } + BodyKind::Chunked => None, + } + } +} + +#[derive(Debug)] +pub struct InvalidContentLength; + +impl fmt::Display for InvalidContentLength { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + "incoming content-length should be a u64; violates HTTP/1.1".fmt(f) + } +} + +impl std::error::Error for InvalidContentLength {} + +impl From for Error { + fn from(e: InvalidContentLength) -> Self { + // TODO: What's the right error code here? + ErrorVariant::Other(e.to_string()).into() + } +} + +/// The output stream for the body, implementing [`AsyncWrite`]. Call +/// [`Responder::start_response`] or [`Client::start_request`] to obtain +/// one. Once the body is complete, it must be declared finished, using +/// [`Finished::finish`], [`Finished::fail`], [`Client::finish`], or +/// [`Client::fail`]. +/// +/// [`Responder::start_response`]: crate::http::server::Responder::start_response +/// [`Client::start_request`]: crate::http::client::Client::start_request +/// [`Finished::finish`]: crate::http::server::Finished::finish +/// [`Finished::fail`]: crate::http::server::Finished::fail +/// [`Client::finish`]: crate::http::client::Client::finish +/// [`Client::fail`]: crate::http::client::Client::fail +#[must_use] +pub struct OutgoingBody { + // IMPORTANT: the order of these fields here matters. `stream` must + // be dropped before `body`. + stream: AsyncOutputStream, + body: wasi::http::types::OutgoingBody, + dontdrop: DontDropOutgoingBody, +} + +impl OutgoingBody { + pub(crate) fn new(stream: AsyncOutputStream, body: wasi::http::types::OutgoingBody) -> Self { + Self { + stream, + body, + dontdrop: DontDropOutgoingBody, + } + } + + pub(crate) fn consume(self) -> (AsyncOutputStream, wasi::http::types::OutgoingBody) { + let Self { + stream, + body, + dontdrop, + } = self; + + std::mem::forget(dontdrop); + + (stream, body) + } + + /// Return a reference to the underlying `AsyncOutputStream`. + /// + /// This usually isn't needed, as `OutgoingBody` implements `AsyncWrite` + /// too, however it is useful for code that expects to work with + /// `AsyncOutputStream` specifically. + pub fn stream(&mut self) -> &mut AsyncOutputStream { + &mut self.stream + } +} + +#[async_trait::async_trait(?Send)] +impl AsyncWrite for OutgoingBody { + async fn write(&mut self, buf: &[u8]) -> crate::io::Result { + self.stream.write(buf).await + } + + async fn flush(&mut self) -> crate::io::Result<()> { + self.stream.flush().await + } + + fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { + Some(&self.stream) + } +} + +/// A utility to ensure that `OutgoingBody` is either finished or failed, and +/// not implicitly dropped. +struct DontDropOutgoingBody; + +impl Drop for DontDropOutgoingBody { + fn drop(&mut self) { + unreachable!("`OutgoingBody::drop` called; `OutgoingBody`s should be consumed with `finish` or `fail`."); + } +} + +/// A placeholder for use as the type parameter to [`Request`] and [`Response`] +/// to indicate that the body has not yet started. This is used with +/// [`Client::start_request`] and [`Responder::start_response`], which have +/// `Requeset` and `Response` arguments, +/// respectively. +/// +/// To instead start the response and obtain the output stream for the body, +/// use [`Responder::respond`]. +/// To instead send a request or response with an input stream for the body, +/// use [`Client::send`] or [`Responder::respond`]. +/// +/// [`Request`]: crate::http::Request +/// [`Response`]: crate::http::Response +/// [`Client::start_request`]: crate::http::Client::start_request +/// [`Responder::start_response`]: crate::http::server::Responder::start_response +/// [`Client::send`]: crate::http::Client::send +/// [`Responder::respond`]: crate::http::server::Responder::respond +pub struct BodyForthcoming; diff --git a/src/http/client.rs b/src/http/client.rs index fd251d6..54bfeea 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -1,21 +1,9 @@ -use super::{ - body::{BodyForthcoming, IncomingBody, OutgoingBody}, - fields::header_map_to_wasi, - Body, Error, HeaderMap, Request, Response, Result, -}; +use super::{body::Incoming, Body, Error, Request, Response, Result}; use crate::http::request::try_into_outgoing; use crate::http::response::try_from_incoming; -use crate::io::{self, AsyncOutputStream, AsyncPollable}; -use crate::runtime::WaitFor; +use crate::io::AsyncPollable; use crate::time::Duration; -use pin_project_lite::pin_project; -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; -use wasip2::http::types::{ - FutureIncomingResponse as WasiFutureIncomingResponse, OutgoingBody as WasiOutgoingBody, - RequestOptions as WasiRequestOptions, -}; +use wasip2::http::types::RequestOptions as WasiRequestOptions; /// An HTTP client. // Empty for now, but permits adding support for RequestOptions soon: @@ -38,26 +26,15 @@ impl Client { /// Send an HTTP request. /// - /// TODO: Should this automatically add a "Content-Length" header if the - /// body size is known? - /// /// To respond with trailers, use [`Client::start_request`] instead. - pub async fn send(&self, req: Request) -> Result> { - // We don't use `body::OutputBody` here because we can report I/O - // errors from the `copy` directly. - let (wasi_req, body) = try_into_outgoing(req)?; - let wasi_body = wasi_req.body().unwrap(); - let wasi_stream = wasi_body.write().unwrap(); + pub async fn send>(&self, req: Request) -> Result> { + let (wasi_req, _body) = try_into_outgoing(req)?; + let _wasi_body = wasi_req.body().unwrap(); // 1. Start sending the request head let res = wasip2::http::outgoing_handler::handle(wasi_req, self.wasi_options()?).unwrap(); - // 2. Start sending the request body - io::copy(body, AsyncOutputStream::new(wasi_stream)).await?; - - // 3. Finish sending the request body - let trailers = None; - WasiOutgoingBody::finish(wasi_body, trailers).unwrap(); + // FIXME send body into wasi_body here // 4. Receive the response AsyncPollable::new(res.subscribe()).wait_for().await; @@ -69,91 +46,6 @@ impl Client { try_from_incoming(res) } - /// Start sending an HTTP request, and return an `OutgoingBody` stream to - /// write the body to. - /// - /// The returned `OutgoingBody` must be consumed by [`Client::finish`] or - /// [`Client::fail`]. - pub async fn start_request( - &self, - req: Request, - ) -> Result<( - OutgoingBody, - impl Future>>, - )> { - let (wasi_req, _body_forthcoming) = try_into_outgoing(req)?; - let wasi_body = wasi_req.body().unwrap(); - let wasi_stream = wasi_body.write().unwrap(); - - // Start sending the request head. - let res = wasip2::http::outgoing_handler::handle(wasi_req, self.wasi_options()?).unwrap(); - - let outgoing_body = OutgoingBody::new(AsyncOutputStream::new(wasi_stream), wasi_body); - - pin_project! { - #[must_use = "futures do nothing unless polled or .awaited"] - struct IncomingResponseFuture { - #[pin] - subscription: WaitFor, - wasi: WasiFutureIncomingResponse, - } - } - impl Future for IncomingResponseFuture { - type Output = Result>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - match this.subscription.poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(()) => Poll::Ready( - this.wasi - .get() - .unwrap() - .unwrap() - .map_err(Error::from) - .and_then(try_from_incoming), - ), - } - } - } - - let subscription = AsyncPollable::new(res.subscribe()).wait_for(); - let future = IncomingResponseFuture { - subscription, - wasi: res, - }; - - Ok((outgoing_body, future)) - } - - /// Finish the body, optionally with trailers. - /// - /// This is used with [`Client::start_request`]. - pub fn finish(body: OutgoingBody, trailers: Option) -> Result<()> { - let (stream, body) = body.consume(); - - // The stream is a child resource of the `OutgoingBody`, so ensure that - // it's dropped first. - drop(stream); - - let wasi_trailers = match trailers { - Some(trailers) => Some(header_map_to_wasi(&trailers)?), - None => None, - }; - - wasip2::http::types::OutgoingBody::finish(body, wasi_trailers) - .expect("body length did not match Content-Length header value"); - Ok(()) - } - - /// Consume the `OutgoingBody` and indicate that the body was not - /// completed. - /// - /// This is used with [`Client::start_request`]. - pub fn fail(body: OutgoingBody) { - let (_stream, _body) = body.consume(); - } - /// Set timeout on connecting to HTTP server pub fn set_connect_timeout(&mut self, d: impl Into) { self.options_mut().connect_timeout = Some(d.into()); diff --git a/src/http/error.rs b/src/http/error.rs index 2eb3922..8140c08 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -112,6 +112,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: super::body::InvalidContentLength) -> Error { + ErrorVariant::Other(e.to_string()).into() + } +} + #[derive(Debug)] pub enum ErrorVariant { WasiHttp(ErrorCode), diff --git a/src/http/mod.rs b/src/http/mod.rs index ef16d13..35ae479 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -4,7 +4,7 @@ pub use http::status::StatusCode; pub use http::uri::{Authority, PathAndQuery, Uri}; #[doc(inline)] -pub use body::{Body, IntoBody}; +pub use body::{util::BodyExt, Body, Incoming as IncomingBody}; pub use client::Client; pub use error::{Error, Result}; pub use fields::{HeaderMap, HeaderName, HeaderValue}; diff --git a/src/http/request.rs b/src/http/request.rs index e307abc..17e3252 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,59 +1,17 @@ use super::{ - body::{BodyKind, IncomingBody}, + body::{BodyHint, Incoming}, error::ErrorCode, fields::{header_map_from_wasi, header_map_to_wasi}, method::{from_wasi_method, to_wasi_method}, scheme::{from_wasi_scheme, to_wasi_scheme}, Authority, Error, HeaderMap, PathAndQuery, Uri, }; -use crate::io::AsyncInputStream; use wasip2::http::outgoing_handler::OutgoingRequest; use wasip2::http::types::IncomingRequest; pub use http::request::{Builder, Request}; -#[cfg(feature = "json")] -use super::{ - body::{BoundedBody, IntoBody}, - error::ErrorVariant, -}; -#[cfg(feature = "json")] -use http::header::{HeaderValue, CONTENT_TYPE}; -#[cfg(feature = "json")] -use serde::Serialize; -#[cfg(feature = "json")] -use serde_json; - -#[cfg(feature = "json")] -pub trait JsonRequest { - fn json(self, json: &T) -> Result>>, Error>; -} - -#[cfg(feature = "json")] -impl JsonRequest for Builder { - /// Send a JSON body. Requires optional `json` feature. - /// - /// Serialization can fail if `T`'s implementation of `Serialize` decides to - /// fail. - #[cfg(feature = "json")] - fn json(self, json: &T) -> Result>>, Error> { - let encoded = serde_json::to_vec(json).map_err(|e| ErrorVariant::Other(e.to_string()))?; - let builder = if !self - .headers_ref() - .is_some_and(|headers| headers.contains_key(CONTENT_TYPE)) - { - self.header( - CONTENT_TYPE, - HeaderValue::from_static("application/json; charset=utf-8"), - ) - } else { - self - }; - builder - .body(encoded.into_body()) - .map_err(|e| ErrorVariant::Other(e.to_string()).into()) - } -} +// TODO: go back and add json stuff??? pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingRequest, T), Error> { let wasi_req = OutgoingRequest::new(header_map_to_wasi(request.headers())?); @@ -97,7 +55,7 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque /// This is used by the `http_server` macro. #[doc(hidden)] -pub fn try_from_incoming(incoming: IncomingRequest) -> Result, ErrorCode> { +pub fn try_from_incoming(incoming: IncomingRequest) -> Result, ErrorCode> { // TODO: What's the right error code to use for invalid headers? let headers: HeaderMap = header_map_from_wasi(incoming.headers()) .map_err(|e| ErrorCode::InternalError(Some(e.to_string())))?; @@ -117,19 +75,14 @@ pub fn try_from_incoming(incoming: IncomingRequest) -> Result Result, Error> { +pub(crate) fn try_from_incoming(incoming: IncomingResponse) -> Result, Error> { let headers: HeaderMap = header_map_from_wasi(incoming.headers())?; // TODO: Does WASI guarantee that the incoming status is valid? let status = StatusCode::from_u16(incoming.status()).map_err(|err| Error::other(err.to_string()))?; - let kind = BodyKind::from_headers(&headers)?; + let hint = BodyHint::from_headers(&headers)?; // `body_stream` is a child of `incoming_body` which means we cannot // drop the parent before we drop the child let incoming_body = incoming .consume() .expect("cannot call `consume` twice on incoming response"); - let body_stream = incoming_body - .stream() - .expect("cannot call `stream` twice on an incoming body"); - - let body = IncomingBody::new(kind, AsyncInputStream::new(body_stream), incoming_body); + let body = Incoming::new(incoming_body, hint); let mut builder = Response::builder().status(status); diff --git a/src/http/server.rs b/src/http/server.rs index d085fa2..81063c9 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -20,13 +20,7 @@ //! [`Response`]: crate::http::Response //! [`http_server`]: crate::http_server -use super::{ - body::{BodyForthcoming, OutgoingBody}, - error::ErrorCode, - fields::header_map_to_wasi, - Body, HeaderMap, Response, -}; -use crate::io::{copy, AsyncOutputStream}; +use super::{error::ErrorCode, fields::header_map_to_wasi, Body, Response}; use http::header::CONTENT_LENGTH; use wasip2::exports::http::incoming_handler::ResponseOutparam; use wasip2::http::types::OutgoingResponse; @@ -45,52 +39,15 @@ pub struct Responder { } impl Responder { - /// Start responding with the given `Response` and return an `OutgoingBody` - /// stream to write the body to. - /// - /// # Example - /// - /// ``` - /// # use wstd::http::{body::{IncomingBody, BodyForthcoming}, Response, Request}; - /// # use wstd::http::server::{Finished, Responder}; - /// # use crate::wstd::io::AsyncWrite; - /// # async fn example(responder: Responder) -> Finished { - /// let mut body = responder.start_response(Response::new(BodyForthcoming)); - /// let result = body - /// .write_all("Hello!\n".as_bytes()) - /// .await; - /// Finished::finish(body, result, None) - /// # } - /// # fn main() {} - /// ``` - pub fn start_response(self, response: Response) -> OutgoingBody { - let wasi_headers = header_map_to_wasi(response.headers()).expect("header error"); - let wasi_response = OutgoingResponse::new(wasi_headers); - let wasi_status = response.status().as_u16(); - - // Unwrap because `StatusCode` has already validated the status. - wasi_response.set_status_code(wasi_status).unwrap(); - - // Unwrap because we can be sure we only call these once. - let wasi_body = wasi_response.body().unwrap(); - let wasi_stream = wasi_body.write().unwrap(); - - // Tell WASI to start the show. - ResponseOutparam::set(self.outparam, Ok(wasi_response)); - - OutgoingBody::new(AsyncOutputStream::new(wasi_stream), wasi_body) - } - /// Respond with the given `Response` which contains the body. /// /// If the body has a known length, a Content-Length header is automatically added. /// - /// To respond with trailers, use [`Responder::start_response`] instead. - /// /// # Example /// /// ``` - /// # use wstd::http::{body::{IncomingBody, BodyForthcoming}, IntoBody, Response, Request}; + /// # use wstd::http::{Response, Request}; + /// # use wstd::http::body::BodyExt; /// # use wstd::http::server::{Finished, Responder}; /// # /// # async fn example(responder: Responder) { @@ -100,17 +57,17 @@ impl Responder { /// # } /// # fn main() {} /// ``` - pub async fn respond(self, response: Response) { + pub async fn respond>(self, response: Response) { let headers = response.headers(); let status = response.status().as_u16(); let wasi_headers = header_map_to_wasi(headers).expect("header error"); // Consume the `response` and prepare to write the body. - let mut body = response.into_body(); + let body = response.into_body().into(); // Automatically add a Content-Length header. - if let Some(len) = body.len() { + if let Some(len) = body.content_length() { let mut buffer = itoa::Buffer::new(); wasi_headers .append(CONTENT_LENGTH.as_str(), buffer.format(len).as_bytes()) @@ -123,18 +80,15 @@ impl Responder { wasi_response.set_status_code(status).unwrap(); // Unwrap because we can be sure we only call these once. - let wasi_body = wasi_response.body().unwrap(); - let wasi_stream = wasi_body.write().unwrap(); + let _wasi_body = wasi_response.body().unwrap(); // Tell WASI to start the show. ResponseOutparam::set(self.outparam, Ok(wasi_response)); - // TODO: Move this out to BodyForthcoming! - let mut outgoing_body = OutgoingBody::new(AsyncOutputStream::new(wasi_stream), wasi_body); - - let result = copy(&mut body, &mut outgoing_body).await; - let trailers = None; - Finished::finish(outgoing_body, result, trailers); + // FIXME: send `body` into `wasi_body` + //let mut outgoing_body = OutgoingBody::new(AsyncOutputStream::new(wasi_stream), wasi_body); + //let result = copy(&mut body, &mut outgoing_body).await; + //let trailers = None; } /// This is used by the `http_server` macro. @@ -149,55 +103,3 @@ impl Responder { ResponseOutparam::set(self.outparam, Err(err)) } } - -/// An opaque value returned from a handler indicating that the body is -/// finished, either by [`Finished::finish`] or [`Finished::fail`]. -pub struct Finished(pub(crate) ()); - -impl Finished { - /// Finish the body, optionally with trailers, and return a `Finished` - /// token to be returned from the [`http_server`] `main` function to indicate - /// that the response is finished. - /// - /// `result` is a `std::io::Result` for reporting any I/O errors that - /// occur while writing to the body stream. - /// - /// [`http_server`]: crate::http_server - pub fn finish( - body: OutgoingBody, - result: std::io::Result<()>, - trailers: Option, - ) -> Self { - let (stream, body) = body.consume(); - - // The stream is a child resource of the `OutgoingBody`, so ensure that - // it's dropped first. - drop(stream); - - // If there was an I/O error, panic and don't call `OutgoingBody::finish`. - result.expect("I/O error while writing the body"); - - let wasi_trailers = - trailers.map(|trailers| header_map_to_wasi(&trailers).expect("header error")); - - wasip2::http::types::OutgoingBody::finish(body, wasi_trailers) - .expect("body length did not match Content-Length header value"); - - Self(()) - } - - /// Return a `Finished` token that can be returned from a handler to - /// indicate that the body is not finished and should be considered - /// corrupted. - pub fn fail(body: OutgoingBody) -> Self { - let (stream, _body) = body.consume(); - - // The stream is a child resource of the `OutgoingBody`, so ensure that - // it's dropped first. - drop(stream); - - // No need to do anything else; omitting the call to `finish` achieves - // the desired effect. - Self(()) - } -} From c3f217780cb9b8c5b04667e90ec1c973ba321df4 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 16 Sep 2025 18:50:45 -0700 Subject: [PATCH 05/13] impl Body --- Cargo.toml | 1 + src/http/body.rs | 208 +++++++++++++++++++++++++++++++++++++++++++---- src/lib.rs | 1 - 3 files changed, 195 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index caafa4c..960a232 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ default = ["json"] json = ["dep:serde", "dep:serde_json"] [dependencies] +anyhow.workspace = true async-task.workspace = true async-trait.workspace = true bytes.workspace = true diff --git a/src/http/body.rs b/src/http/body.rs index 6b35b04..5f82922 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -1,6 +1,6 @@ -use crate::http::HeaderMap; +use crate::http::{fields::header_map_from_wasi, HeaderMap}; use crate::io::AsyncInputStream; -use crate::runtime::{AsyncPollable, WaitFor}; +use crate::runtime::{AsyncPollable, Reactor, WaitFor}; pub use ::http_body::{Body as HttpBody, Frame, SizeHint}; pub use bytes::Bytes; @@ -11,8 +11,8 @@ use std::fmt; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -use wasip2::http::types::{ErrorCode, Fields, FutureTrailers, IncomingBody as WasiIncomingBody}; -use wasip2::io::streams::StreamError; +use wasip2::http::types::{FutureTrailers, IncomingBody as WasiIncomingBody}; +use wasip2::io::streams::{InputStream as WasiInputStream, StreamError}; pub mod util { pub use http_body_util::*; @@ -67,7 +67,7 @@ impl Incoming { } /// Use with `http_body::Body` trait pub fn into_http_body(self) -> IncomingBody { - IncomingBody::new(self) + IncomingBody::new(self.body, self.size_hint) } pub fn into_input_stream(self) -> AsyncInputStream { AsyncInputStream::new( @@ -113,19 +113,199 @@ impl fmt::Display for InvalidContentLength { impl std::error::Error for InvalidContentLength {} pub struct IncomingBody { - stream: Option, - body: Option, + state: Option>>, size_hint: BodyHint, } +impl IncomingBody { + fn new(body: WasiIncomingBody, size_hint: BodyHint) -> Self { + Self { + state: Some(Box::pin(IncomingBodyState::Body { + read_state: BodyState { + wait: None, + subscription: None, + stream: body + .stream() + .expect("wasi incoming-body stream should not yet be taken"), + }, + body: Some(body), + })), + size_hint, + } + } +} + +impl HttpBody for IncomingBody { + type Data = Bytes; + type Error = anyhow::Error; + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + loop { + let state = self.as_mut().state.take(); + if state.is_none() { + return Poll::Ready(None); + } + let mut state = state.unwrap(); + match state.as_mut().project() { + IBSProj::Body { read_state, body } => match read_state.poll_frame(cx) { + Poll::Pending => { + self.as_mut().state = Some(state); + return Poll::Pending; + } + Poll::Ready(Some(r)) => { + self.as_mut().state = Some(state); + return Poll::Ready(Some(r)); + } + Poll::Ready(None) => { + let trailers_state = TrailersState::new(WasiIncomingBody::finish( + body.take().expect("finishing Body state"), + )); + self.as_mut().state = + Some(Box::pin(IncomingBodyState::Trailers { trailers_state })); + continue; + } + }, + IBSProj::Trailers { trailers_state } => match trailers_state.poll_frame(cx) { + Poll::Pending => { + self.as_mut().state = Some(state); + return Poll::Pending; + } + Poll::Ready(r) => return Poll::Ready(r), + }, + } + } + } + fn is_end_stream(&self) -> bool { + self.state.is_none() + } + fn size_hint(&self) -> SizeHint { + match self.size_hint { + BodyHint::ContentLength(l) => SizeHint::with_exact(l), + _ => Default::default(), + } + } +} + +pin_project_lite::pin_project! { +#[project = IBSProj] enum IncomingBodyState { - Body(WasiIncomingBody), - FutureTrailers(FutureTrailers), - Trailers(Result, ErrorCode>), + Body { #[pin] read_state: BodyState, body: Option /* Some until destroying */ }, + Trailers { #[pin] trailers_state: TrailersState }, +} } -struct ReadState { - read_op: Option, StreamError>>>>>, - stream: AsyncInputStream, - body: WasiIncomingBody, +struct BodyState { + wait: Option>>, + subscription: Option, + // Always some unless finishing + stream: WasiInputStream, +} + +const MAX_FRAME_SIZE: u64 = 64 * 1024; + +impl BodyState { + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, anyhow::Error>>> { + loop { + match self.stream.read(MAX_FRAME_SIZE) { + Ok(bs) if !bs.is_empty() => { + return Poll::Ready(Some(Ok(Frame::data(Bytes::from(bs))))) + } + Err(StreamError::Closed) => return Poll::Ready(None), + Err(StreamError::LastOperationFailed(err)) => { + return Poll::Ready(Some(Err(anyhow::anyhow!( + "error reading incoming body: {}", + err.to_debug_string() + )))) + } + Ok(_empty) => { + if self.subscription.is_none() { + self.as_mut().subscription = + Some(Reactor::current().schedule(self.stream.subscribe())); + } + if self.wait.is_none() { + let wait = self.as_ref().subscription.as_ref().unwrap().wait_for(); + self.as_mut().wait = Some(Box::pin(wait)); + } + let mut taken_wait = self.as_mut().wait.take().unwrap(); + match taken_wait.as_mut().poll(cx) { + Poll::Pending => { + self.as_mut().wait = Some(taken_wait); + return Poll::Pending; + } + // Its possible that, after returning ready, the + // stream does not actually provide any input. This + // behavior should only occur once. + Poll::Ready(()) => { + continue; + } + } + } + } + } + } +} + +struct TrailersState { + wait: Option>>, + subscription: Option, + future_trailers: FutureTrailers, +} + +impl TrailersState { + fn new(future_trailers: FutureTrailers) -> Self { + Self { + wait: None, + subscription: None, + future_trailers, + } + } + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, anyhow::Error>>> { + loop { + if let Some(ready) = self.future_trailers.get() { + return match ready { + Ok(Ok(Some(trailers))) => match header_map_from_wasi(trailers) { + Ok(header_map) => Poll::Ready(Some(Ok(Frame::trailers(header_map)))), + Err(e) => Poll::Ready(Some(Err(e + .context("decoding incoming body trailers") + .into()))), + }, + Ok(Ok(None)) => Poll::Ready(None), + Ok(Err(e)) => Poll::Ready(Some(Err( + anyhow::Error::from(e).context("recieving incoming body trailers") + ))), + Err(()) => unreachable!("future_trailers.get with some called at most once"), + }; + } + if self.subscription.is_none() { + self.as_mut().subscription = + Some(Reactor::current().schedule(self.future_trailers.subscribe())); + } + if self.wait.is_none() { + let wait = self.as_ref().subscription.as_ref().unwrap().wait_for(); + self.as_mut().wait = Some(Box::pin(wait)); + } + let mut taken_wait = self.as_mut().wait.take().unwrap(); + match taken_wait.as_mut().poll(cx) { + Poll::Pending => { + self.as_mut().wait = Some(taken_wait); + return Poll::Pending; + } + // Its possible that, after returning ready, the + // future_trailers.get() does not actually provide any input. This + // behavior should only occur once. + Poll::Ready(()) => { + continue; + } + } + } + } } diff --git a/src/lib.rs b/src/lib.rs index ccbc2c1..c996866 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,7 +76,6 @@ pub use wasip2; pub mod prelude { pub use crate::future::FutureExt as _; - pub use crate::http::Body as _; pub use crate::io::AsyncRead as _; pub use crate::io::AsyncWrite as _; } From d819cf999f9db7100e9439ff2aee650a524d4f21 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Wed, 17 Sep 2025 21:30:10 -0700 Subject: [PATCH 06/13] body trait is working! --- Cargo.toml | 1 + examples/http_server.rs | 51 +++++++++++------ src/http/body.rs | 118 ++++++++++++++++++++++++++++++++------- src/http/fields.rs | 7 +++ src/http/server.rs | 16 ++++-- src/runtime/reactor.rs | 49 ++++++++-------- test-programs/Cargo.toml | 1 + 7 files changed, 179 insertions(+), 64 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 960a232..997be83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ serde_json = { workspace = true, optional = true } [dev-dependencies] anyhow.workspace = true clap.workspace = true +http-body-util.workspace = true futures-lite.workspace = true futures-concurrency.workspace = true humantime.workspace = true diff --git a/examples/http_server.rs b/examples/http_server.rs index 27c403f..22c9b5f 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -1,14 +1,15 @@ use anyhow::Result; -use wstd::http::body::{BodyForthcoming, BoxBody, IncomingBody, OutgoingBody}; -use wstd::http::{error::ErrorCode, Body, IntoBody, Request, Response, StatusCode}; +use wstd::http::body::{Body, Bytes, Frame, Incoming}; +use wstd::http::{error::ErrorCode, Request, Response, StatusCode}; use wstd::io::{copy, empty, AsyncWrite}; use wstd::time::{Duration, Instant}; #[wstd::http_server] -async fn main(request: Request) -> Result, ErrorCode> { +async fn main(request: Request) -> Result, ErrorCode> { match request.uri().path_and_query().unwrap().as_str() { + "/wait_response" => http_wait_response(request).await, + "/wait_body" => http_wait_body(request).await, /* - "/wait" => http_wait(request, responder).await, "/echo" => http_echo(request, responder).await, "/echo-headers" => http_echo_headers(request, responder).await, "/echo-trailers" => http_echo_trailers(request, responder).await, @@ -24,15 +25,14 @@ async fn main(request: Request) -> Result, Error }) } -async fn http_home(_request: Request) -> Result> { +async fn http_home(_request: Request) -> Result> { // To send a single string as the response body, use `Responder::respond`. Ok(Response::new( - "Hello, wasi:http/proxy world!\n".into_boxed_body(), + "Hello, wasi:http/proxy world!\n".to_owned().into(), )) } -/* -async fn http_wait(_request: Request, responder: Responder) -> Finished { +async fn http_wait_response(_request: Request) -> Result> { // Get the time now let now = Instant::now(); @@ -42,14 +42,33 @@ async fn http_wait(_request: Request, responder: Responder) -> Fin // Compute how long we slept for. let elapsed = Instant::now().duration_since(now).as_millis(); - // To stream data to the response body, use `Responder::start_response`. - let mut body = responder.start_response(Response::new(BodyForthcoming)); - let result = body - .write_all(format!("slept for {elapsed} millis\n").as_bytes()) - .await; - Finished::finish(body, result, None) + Ok(Response::new( + format!("slept for {elapsed} millis\n").into(), + )) +} + +async fn http_wait_body(_request: Request) -> Result> { + use futures_lite::stream::once_future; + use http_body_util::{BodyExt, StreamBody}; + + // Get the time now + let now = Instant::now(); + + let body = StreamBody::new(once_future(async move { + // Sleep for one second. + wstd::task::sleep(Duration::from_secs(1)).await; + + // Compute how long we slept for. + let elapsed = Instant::now().duration_since(now).as_millis(); + anyhow::Ok(Frame::data(Bytes::from(format!( + "slept for {elapsed} millis\n" + )))) + })); + + Ok(Response::new(body.into())) } +/* async fn http_echo(mut request: Request, responder: Responder) -> Finished { // Stream data from the request body to the response body. let mut body = responder.start_response(Response::new(BodyForthcoming)); @@ -93,10 +112,10 @@ async fn http_echo_trailers(request: Request, responder: Responder } */ -async fn http_not_found(_request: Request) -> Result> { +async fn http_not_found(_request: Request) -> Result> { let response = Response::builder() .status(StatusCode::NOT_FOUND) - .body(empty().into_boxed_body()) + .body("".to_owned().into()) .unwrap(); Ok(response) } diff --git a/src/http/body.rs b/src/http/body.rs index 5f82922..6a73097 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -1,17 +1,23 @@ -use crate::http::{fields::header_map_from_wasi, HeaderMap}; -use crate::io::AsyncInputStream; +use crate::http::{ + fields::{header_map_from_wasi, header_map_to_wasi}, + HeaderMap, +}; +use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncWrite}; use crate::runtime::{AsyncPollable, Reactor, WaitFor}; pub use ::http_body::{Body as HttpBody, Frame, SizeHint}; pub use bytes::Bytes; +use anyhow::{Context as _, Error}; use http::header::CONTENT_LENGTH; use http_body_util::combinators::BoxBody; use std::fmt; -use std::future::Future; -use std::pin::Pin; +use std::future::{poll_fn, Future}; +use std::pin::{pin, Pin}; use std::task::{Context, Poll}; -use wasip2::http::types::{FutureTrailers, IncomingBody as WasiIncomingBody}; +use wasip2::http::types::{ + FutureTrailers, IncomingBody as WasiIncomingBody, OutgoingBody as WasiOutgoingBody, +}; use wasip2::io::streams::{InputStream as WasiInputStream, StreamError}; pub mod util { @@ -20,15 +26,81 @@ pub mod util { pub struct Body(pub(crate) BodyInner); pub(crate) enum BodyInner { - Boxed(BoxBody>), + Boxed(BoxBody), Incoming(Incoming), } +impl Body { + pub async fn send(self, outgoing_body: WasiOutgoingBody) -> Result<(), Error> { + match self.0 { + BodyInner::Incoming(incoming) => { + let in_body = incoming.into_inner(); + let mut in_stream = + AsyncInputStream::new(in_body.stream().expect("incoming body already read")); + let mut out_stream = AsyncOutputStream::new( + outgoing_body + .write() + .expect("outgoing body already written"), + ); + crate::io::copy(&mut in_stream, &mut out_stream) + .await + .context("copying incoming body stream to outgoing body stream")?; + drop(in_stream); + drop(out_stream); + let future_in_trailers = WasiIncomingBody::finish(in_body); + Reactor::current() + .schedule(future_in_trailers.subscribe()) + .wait_for() + .await; + let in_trailers: Option = future_in_trailers + .get() + .expect("pollable ready") + .expect("got once") + .context("recieving incoming trailers")?; + WasiOutgoingBody::finish(outgoing_body, in_trailers) + .context("finishing outgoing body")?; + Ok(()) + } + BodyInner::Boxed(box_body) => { + let mut out_stream = AsyncOutputStream::new( + outgoing_body + .write() + .expect("outgoing body already written"), + ); + let mut body = pin!(box_body); + let mut trailers = None; + loop { + match poll_fn(|cx| body.as_mut().poll_frame(cx)).await { + Some(Ok(frame)) if frame.is_data() => { + let data = frame.data_ref().unwrap(); + out_stream.write_all(data).await?; + } + Some(Ok(frame)) if frame.is_trailers() => { + trailers = Some( + header_map_to_wasi(frame.trailers_ref().unwrap()) + .context("outoging trailers to wasi")?, + ); + } + Some(Err(err)) => break Err(err.context("sending outgoing body")), + None => { + drop(out_stream); + WasiOutgoingBody::finish(outgoing_body, trailers) + .context("finishing outgoing body")?; + break Ok(()); + } + _ => unreachable!(), + } + } + } + } + } +} + impl From for Body where B: HttpBody + Send + Sync + 'static, ::Data: Into, - ::Error: Into>, + ::Error: Into, { fn from(http_body: B) -> Body { use util::BodyExt; @@ -41,6 +113,10 @@ where } } +// TODO: +// We can fill out a bunch of convenient From impls for Body - From<&str>, +// From, From<&[u8]>, From>, From, From<()>, etc etc + impl From for Body { fn from(incoming: Incoming) -> Body { Body(BodyInner::Incoming(incoming)) @@ -69,12 +145,8 @@ impl Incoming { pub fn into_http_body(self) -> IncomingBody { IncomingBody::new(self.body, self.size_hint) } - pub fn into_input_stream(self) -> AsyncInputStream { - AsyncInputStream::new( - self.body - .stream() - .expect("incoming body stream has not been taken"), - ) + pub fn into_inner(self) -> WasiIncomingBody { + self.body } } @@ -189,17 +261,25 @@ impl HttpBody for IncomingBody { } pin_project_lite::pin_project! { -#[project = IBSProj] -enum IncomingBodyState { - Body { #[pin] read_state: BodyState, body: Option /* Some until destroying */ }, - Trailers { #[pin] trailers_state: TrailersState }, -} + #[project = IBSProj] + enum IncomingBodyState { + Body { + #[pin] + read_state: BodyState, + // body is Some until we need to remove it from a projection + // during a state transition + body: Option + }, + Trailers { + #[pin] + trailers_state: TrailersState + }, + } } struct BodyState { wait: Option>>, subscription: Option, - // Always some unless finishing stream: WasiInputStream, } diff --git a/src/http/fields.rs b/src/http/fields.rs index 34452f5..f476cc9 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -1,6 +1,7 @@ pub use http::header::{HeaderMap, HeaderName, HeaderValue}; use super::Error; +use core::fmt; use wasip2::http::types::{Fields, HeaderError as WasiHttpHeaderError}; pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { @@ -34,3 +35,9 @@ pub(crate) struct ToWasiHeaderError { pub(crate) error: WasiHttpHeaderError, pub(crate) context: String, } +impl fmt::Display for ToWasiHeaderError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + core::write!(f, "{}: {:?}", self.context, self.error) + } +} +impl core::error::Error for ToWasiHeaderError {} diff --git a/src/http/server.rs b/src/http/server.rs index 81063c9..db8b420 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -80,15 +80,21 @@ impl Responder { wasi_response.set_status_code(status).unwrap(); // Unwrap because we can be sure we only call these once. - let _wasi_body = wasi_response.body().unwrap(); + let wasi_body = wasi_response.body().unwrap(); // Tell WASI to start the show. ResponseOutparam::set(self.outparam, Ok(wasi_response)); - // FIXME: send `body` into `wasi_body` - //let mut outgoing_body = OutgoingBody::new(AsyncOutputStream::new(wasi_stream), wasi_body); - //let result = copy(&mut body, &mut outgoing_body).await; - //let trailers = None; + crate::runtime::spawn(async move { + if let Err(e) = body.send(wasi_body).await { + // FIXME we have no way to actually report this, which kinda + // sucks. we can make this function return a Task and have the + // macro detatch and eprintln it to at least make better + // things possible in a better embedding?? + eprintln!("Outgoing response body error: {e:?}") + } + }) + .detach(); } /// This is used by the `http_server` macro. diff --git a/src/runtime/reactor.rs b/src/runtime/reactor.rs index 67d1de2..ba78fd9 100644 --- a/src/runtime/reactor.rs +++ b/src/runtime/reactor.rs @@ -1,13 +1,12 @@ use super::REACTOR; use async_task::{Runnable, Task}; -use core::cell::RefCell; use core::future::Future; use core::pin::Pin; use core::task::{Context, Poll, Waker}; use slab::Slab; use std::collections::{HashMap, VecDeque}; -use std::rc::Rc; +use std::sync::{Arc, Mutex}; use wasip2::io::poll::Pollable; /// A key for a `Pollable`, which is an index into the `Slab` in `Reactor`. @@ -31,7 +30,7 @@ impl Drop for Registration { /// An AsyncPollable is a reference counted Registration. It can be cloned, and used to create /// as many WaitFor futures on a Pollable that the user needs. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct AsyncPollable(Rc); +pub struct AsyncPollable(Arc); impl AsyncPollable { /// Create an `AsyncPollable` from a Wasi `Pollable`. Schedules the `Pollable` with the current @@ -92,16 +91,16 @@ impl Drop for WaitFor { /// Manage async system resources for WASI 0.2 #[derive(Debug, Clone)] pub struct Reactor { - inner: Rc, + inner: Arc, } /// The private, internal `Reactor` implementation - factored out so we can take /// a lock of the whole. #[derive(Debug)] struct InnerReactor { - pollables: RefCell>, - wakers: RefCell>, - ready_list: RefCell>, + pollables: Mutex>, + wakers: Mutex>, + ready_list: Mutex>, } impl Reactor { @@ -121,10 +120,10 @@ impl Reactor { /// Create a new instance of `Reactor` pub(crate) fn new() -> Self { Self { - inner: Rc::new(InnerReactor { - pollables: RefCell::new(Slab::new()), - wakers: RefCell::new(HashMap::new()), - ready_list: RefCell::new(VecDeque::new()), + inner: Arc::new(InnerReactor { + pollables: Mutex::new(Slab::new()), + wakers: Mutex::new(HashMap::new()), + ready_list: Mutex::new(VecDeque::new()), }), } } @@ -133,7 +132,7 @@ impl Reactor { /// Future pending on their readiness. This function returns indicating /// that set of pollables is not empty. pub(crate) fn pending_pollables_is_empty(&self) -> bool { - self.inner.wakers.borrow().is_empty() + self.inner.wakers.lock().unwrap().is_empty() } /// Block until at least one pending pollable is ready, waking a pending future. @@ -189,8 +188,8 @@ impl Reactor { where F: FnOnce(&[&Pollable]) -> Vec, { - let wakers = self.inner.wakers.borrow(); - let pollables = self.inner.pollables.borrow(); + let wakers = self.inner.wakers.lock().unwrap(); + let pollables = self.inner.pollables.lock().unwrap(); // We're about to wait for a number of pollables. When they wake we get // the *indexes* back for the pollables whose events were available - so @@ -225,18 +224,18 @@ impl Reactor { /// Turn a Wasi [`Pollable`] into an [`AsyncPollable`] pub fn schedule(&self, pollable: Pollable) -> AsyncPollable { - let mut pollables = self.inner.pollables.borrow_mut(); + let mut pollables = self.inner.pollables.lock().unwrap(); let key = EventKey(pollables.insert(pollable)); - AsyncPollable(Rc::new(Registration { key })) + AsyncPollable(Arc::new(Registration { key })) } fn deregister_event(&self, key: EventKey) { - let mut pollables = self.inner.pollables.borrow_mut(); + let mut pollables = self.inner.pollables.lock().unwrap(); pollables.remove(key.0); } fn deregister_waitee(&self, waitee: &Waitee) { - let mut wakers = self.inner.wakers.borrow_mut(); + let mut wakers = self.inner.wakers.lock().unwrap(); wakers.remove(waitee); } @@ -244,14 +243,16 @@ impl Reactor { let ready = self .inner .pollables - .borrow() + .lock() + .unwrap() .get(waitee.pollable.0.key.0) .expect("only live EventKey can be checked for readiness") .ready(); if !ready { self.inner .wakers - .borrow_mut() + .lock() + .unwrap() .insert(waitee.clone(), waker.clone()); } ready @@ -264,7 +265,7 @@ impl Reactor { T: 'static, { let this = self.clone(); - let schedule = move |runnable| this.inner.ready_list.borrow_mut().push_back(runnable); + let schedule = move |runnable| this.inner.ready_list.lock().unwrap().push_back(runnable); // SAFETY: // we're using this exactly like async_task::spawn_local, except that @@ -273,16 +274,16 @@ impl Reactor { // single-threaded. #[allow(unsafe_code)] let (runnable, task) = unsafe { async_task::spawn_unchecked(fut, schedule) }; - self.inner.ready_list.borrow_mut().push_back(runnable); + self.inner.ready_list.lock().unwrap().push_back(runnable); task } pub(super) fn pop_ready_list(&self) -> Option { - self.inner.ready_list.borrow_mut().pop_front() + self.inner.ready_list.lock().unwrap().pop_front() } pub(super) fn ready_list_is_empty(&self) -> bool { - self.inner.ready_list.borrow().is_empty() + self.inner.ready_list.lock().unwrap().is_empty() } } diff --git a/test-programs/Cargo.toml b/test-programs/Cargo.toml index d9b97e0..94eaa2a 100644 --- a/test-programs/Cargo.toml +++ b/test-programs/Cargo.toml @@ -9,5 +9,6 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true futures-lite.workspace = true +http-body-util.workspace = true serde_json.workspace = true wstd.workspace = true From e97b22cea203e139be1f9eeb1f83a80e6afec68e Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 19 Sep 2025 11:16:25 -0700 Subject: [PATCH 07/13] complete rewriting http_server example and its test harness --- examples/http_server.rs | 105 +++++++------- src/http/body.rs | 8 +- test-programs/artifacts/tests/http_server.rs | 139 +++++++++++++++---- 3 files changed, 172 insertions(+), 80 deletions(-) diff --git a/examples/http_server.rs b/examples/http_server.rs index 22c9b5f..47d4c1f 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -1,22 +1,24 @@ -use anyhow::Result; +use anyhow::{Context, Result}; +use futures_lite::stream::once_future; +use http_body_util::{BodyExt, StreamBody}; use wstd::http::body::{Body, Bytes, Frame, Incoming}; -use wstd::http::{error::ErrorCode, Request, Response, StatusCode}; -use wstd::io::{copy, empty, AsyncWrite}; +use wstd::http::{error::ErrorCode, HeaderMap, Request, Response, StatusCode}; use wstd::time::{Duration, Instant}; #[wstd::http_server] async fn main(request: Request) -> Result, ErrorCode> { - match request.uri().path_and_query().unwrap().as_str() { - "/wait_response" => http_wait_response(request).await, - "/wait_body" => http_wait_body(request).await, - /* - "/echo" => http_echo(request, responder).await, - "/echo-headers" => http_echo_headers(request, responder).await, - "/echo-trailers" => http_echo_trailers(request, responder).await, - "/fail" => http_fail(request, responder).await, - "/bigfail" => http_bigfail(request, responder).await, - */ + let path = request.uri().path_and_query().unwrap().as_str(); + println!("serving {path}"); + match path { "/" => http_home(request).await, + "/wait-response" => http_wait_response(request).await, + "/wait-body" => http_wait_body(request).await, + "/echo" => http_echo(request).await, + "/echo-headers" => http_echo_headers(request).await, + "/echo-trailers" => http_echo_trailers(request).await, + "/response-status" => http_response_status(request).await, + "/response-fail" => http_response_fail(request).await, + "/response-body-fail" => http_body_fail(request).await, _ => http_not_found(request).await, } .map_err(|e| match e.downcast::() { @@ -48,9 +50,6 @@ async fn http_wait_response(_request: Request) -> Result) -> Result> { - use futures_lite::stream::once_future; - use http_body_util::{BodyExt, StreamBody}; - // Get the time now let now = Instant::now(); @@ -68,49 +67,57 @@ async fn http_wait_body(_request: Request) -> Result> { Ok(Response::new(body.into())) } -/* -async fn http_echo(mut request: Request, responder: Responder) -> Finished { - // Stream data from the request body to the response body. - let mut body = responder.start_response(Response::new(BodyForthcoming)); - let result = copy(request.body_mut(), &mut body).await; - Finished::finish(body, result, None) +async fn http_echo(request: Request) -> Result> { + let (_parts, body) = request.into_parts(); + Ok(Response::new(body.into())) } -async fn http_fail(_request: Request, responder: Responder) -> Finished { - let body = responder.start_response(Response::new(BodyForthcoming)); - Finished::fail(body) +async fn http_echo_headers(request: Request) -> Result> { + let mut response = Response::builder(); + *response.headers_mut().unwrap() = request.into_parts().0.headers; + Ok(response.body("".to_owned().into())?) } -async fn http_bigfail(_request: Request, responder: Responder) -> Finished { - async fn write_body(body: &mut OutgoingBody) -> wstd::io::Result<()> { - for _ in 0..0x10 { - body.write_all("big big big big\n".as_bytes()).await?; - } - body.flush().await?; - Ok(()) - } +async fn http_echo_trailers(request: Request) -> Result> { + let collected = request.into_body().into_http_body().collect().await?; + let trailers = collected.trailers().cloned().unwrap_or_else(|| { + let mut trailers = HeaderMap::new(); + trailers.insert("x-no-trailers", "1".parse().unwrap()); + trailers + }); - let mut body = responder.start_response(Response::new(BodyForthcoming)); - let _ = write_body(&mut body).await; - Finished::fail(body) + let body = StreamBody::new(once_future(async move { + anyhow::Ok(Frame::::trailers(trailers)) + })); + Ok(Response::new(body.into())) } -async fn http_echo_headers(request: Request, responder: Responder) -> Finished { - let mut response = Response::builder(); - *response.headers_mut().unwrap() = request.into_parts().0.headers; - let response = response.body(empty()).unwrap(); - responder.respond(response).await +async fn http_response_status(request: Request) -> Result> { + let status = if let Some(header_val) = request.headers().get("x-response-status") { + header_val + .to_str() + .context("contents of x-response-status")? + .parse::() + .context("u16 value from x-response-status")? + } else { + 500 + }; + Ok(Response::builder() + .status(status) + .body(String::new().into())?) } -async fn http_echo_trailers(request: Request, responder: Responder) -> Finished { - let body = responder.start_response(Response::new(BodyForthcoming)); - let (trailers, result) = match request.into_body().finish().await { - Ok(trailers) => (trailers, Ok(())), - Err(err) => (Default::default(), Err(std::io::Error::other(err))), - }; - Finished::finish(body, result, trailers) +async fn http_response_fail(_request: Request) -> Result> { + Err(anyhow::anyhow!("error creating response")) +} + +async fn http_body_fail(_request: Request) -> Result> { + let body = StreamBody::new(once_future(async move { + Err::, _>(anyhow::anyhow!("error creating body")) + })); + + Ok(Response::new(body.into())) } -*/ async fn http_not_found(_request: Request) -> Result> { let response = Response::builder() diff --git a/src/http/body.rs b/src/http/body.rs index 6a73097..57d1d09 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -231,9 +231,11 @@ impl HttpBody for IncomingBody { return Poll::Ready(Some(r)); } Poll::Ready(None) => { - let trailers_state = TrailersState::new(WasiIncomingBody::finish( - body.take().expect("finishing Body state"), - )); + // state contains children of the incoming-body. Must drop it + // in order to finish + let body = body.take().expect("finishing Body state"); + drop(state); + let trailers_state = TrailersState::new(WasiIncomingBody::finish(body)); self.as_mut().state = Some(Box::pin(IncomingBodyState::Trailers { trailers_state })); continue; diff --git a/test-programs/artifacts/tests/http_server.rs b/test-programs/artifacts/tests/http_server.rs index 7823550..7e60465 100644 --- a/test-programs/artifacts/tests/http_server.rs +++ b/test-programs/artifacts/tests/http_server.rs @@ -1,21 +1,31 @@ use anyhow::Result; -use std::process::Command; +use std::net::TcpStream; +use std::process::{Child, Command}; +use std::thread::sleep; +use std::time::{Duration, Instant}; + +// Wasmtime serve will run until killed. Kill it in a drop impl so the process +// isnt orphaned when the test suite ends (successfully, or unsuccessfully) +struct DontOrphan(Child); +impl Drop for DontOrphan { + fn drop(&mut self) { + let _ = self.0.kill(); + } +} #[test_log::test] fn http_server() -> Result<()> { - use std::net::TcpStream; - use std::thread::sleep; - use std::time::Duration; - // Run wasmtime serve. // Enable -Scli because we currently don't have a way to build with the // proxy adapter, so we build with the default adapter. - let mut wasmtime_process = Command::new("wasmtime") - .arg("serve") - .arg("-Scli") - .arg("--addr=127.0.0.1:8081") - .arg(test_programs_artifacts::HTTP_SERVER) - .spawn()?; + let _wasmtime_process = DontOrphan( + Command::new("wasmtime") + .arg("serve") + .arg("-Scli") + .arg("--addr=127.0.0.1:8081") + .arg(test_programs_artifacts::HTTP_SERVER) + .spawn()?, + ); // Clumsily wait for the server to accept connections. 'wait: loop { @@ -25,28 +35,63 @@ fn http_server() -> Result<()> { } } - // Do some tests! + // Test each path in the server: + // TEST / http_home + // Response body is the hard-coded default let body: String = ureq::get("http://127.0.0.1:8081").call()?.into_string()?; assert_eq!(body, "Hello, wasi:http/proxy world!\n"); - match ureq::get("http://127.0.0.1:8081/fail").call() { - Ok(body) => { - panic!("unexpected success from /fail: {:?}", body); - } - Err(ureq::Error::Transport(_transport)) => {} - Err(other) => { - panic!("unexpected error: {:?}", other); - } - } + // TEST /wait-response http_wait_response + // Sleeps for 1 second, then sends a response with body containing + // internally measured sleep time. + let start = Instant::now(); + let body: String = ureq::get("http://127.0.0.1:8081/wait-response") + .call()? + .into_string()?; + let duration = start.elapsed(); + let sleep_report = body + .split(' ') + .find_map(|s| s.parse::().ok()) + .expect("body should print 'slept for 10xx millis'"); + assert!( + sleep_report >= 1000, + "should have slept for 1000 or more millis, got {sleep_report}" + ); + assert!(duration >= Duration::from_secs(1)); - const MESSAGE: &[u8] = b"hello, echoserver!\n"; + // TEST /wait-body http_wait_body + // Sends response status and headers, then sleeps for 1 second, then sends + // body with internally measured sleep time. + // With ureq we can't tell that the response status and headers were sent + // with a delay in the body. Additionally, the implementation MAY buffer up the + // entire response and body before sending it, though wasmtime does not. + let start = Instant::now(); + let body: String = ureq::get("http://127.0.0.1:8081/wait-body") + .call()? + .into_string()?; + let duration = start.elapsed(); + let sleep_report = body + .split(' ') + .find_map(|s| s.parse::().ok()) + .expect("body should print 'slept for 10xx millis'"); + assert!( + sleep_report >= 1000, + "should have slept for 1000 or more millis, got {sleep_report}" + ); + assert!(duration >= Duration::from_secs(1)); + // TEST /echo htto_echo + // Send a request body, see that we got the same back in response body. + const MESSAGE: &[u8] = b"hello, echoserver!\n"; let body: String = ureq::get("http://127.0.0.1:8081/echo") .send(MESSAGE)? .into_string()?; assert_eq!(body.as_bytes(), MESSAGE); + // TEST /echo-headers htto_echo_headers + // Send request with headers, see that all of those headers are present in + // response headers let test_headers = [ ("Red", "Rhubarb"), ("Orange", "Carrots"), @@ -55,19 +100,57 @@ fn http_server() -> Result<()> { ("Blue", "Blueberries"), ("Purple", "Beets"), ]; - - let mut response = ureq::get("http://127.0.0.1:8081/echo-headers"); + let mut request = ureq::get("http://127.0.0.1:8081/echo-headers"); for (name, value) in test_headers { - response = response.set(name, value); + request = request.set(name, value); } - let response = response.call()?; - + let response = request.call()?; assert!(response.headers_names().len() >= test_headers.len()); for (name, value) in test_headers { assert_eq!(response.header(name), Some(value)); } - wasmtime_process.kill()?; + // NOT TESTED /echo-trailers htto_echo_trailers + // ureq doesn't support trailers + + // TEST /response-code http_response_code + // Send request with `X-Request-Code: `. Should get back that + // status. + let response = ureq::get("http://127.0.0.1:8081/response-status") + .set("X-Response-Status", "302") + .call()?; + assert_eq!(response.status(), 302); + + let response = ureq::get("http://127.0.0.1:8081/response-status") + .set("X-Response-Status", "401") + .call(); + // ureq interprets some statuses as OK, some as Err: + match response { + Err(ureq::Error::Status(401, _)) => {} + result => { + panic!("/response-code expected status 302, got: {result:?}"); + } + } + + // TEST /response-fail http_response_fail + // Wasmtime gives a 500 error when wasi-http guest gives error instead of + // response + match ureq::get("http://127.0.0.1:8081/response-fail").call() { + Err(ureq::Error::Status(500, _)) => {} + result => { + panic!("/response-fail expected status 500 error, got: {result:?}"); + } + } + + // TEST /response-body-fail http_body_fail + // Response status and headers sent off, then error in body will close + // connection + match ureq::get("http://127.0.0.1:8081/response-body-fail").call() { + Err(ureq::Error::Transport(_transport)) => {} + result => { + panic!("/response-body-fail expected transport error, got: {result:?}") + } + } Ok(()) } From c989145dfea88a25019f434b84fa352c95273390 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Fri, 19 Sep 2025 12:51:54 -0700 Subject: [PATCH 08/13] client typechecks, with some problems --- Cargo.toml | 2 +- examples/http_server.rs | 2 +- src/http/body.rs | 112 +++++++++++++++++++++++++++---- src/http/client.rs | 45 ++++++++----- tests/http_first_byte_timeout.rs | 5 +- 5 files changed, 131 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 997be83..63c11be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ anyhow.workspace = true async-task.workspace = true async-trait.workspace = true bytes.workspace = true +futures-lite.workspace = true http.workspace = true http-body.workspace = true http-body-util.workspace = true @@ -38,7 +39,6 @@ serde_json = { workspace = true, optional = true } anyhow.workspace = true clap.workspace = true http-body-util.workspace = true -futures-lite.workspace = true futures-concurrency.workspace = true humantime.workspace = true serde = { workspace = true, features = ["derive"] } diff --git a/examples/http_server.rs b/examples/http_server.rs index 47d4c1f..37eaadb 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -122,7 +122,7 @@ async fn http_body_fail(_request: Request) -> Result> { async fn http_not_found(_request: Request) -> Result> { let response = Response::builder() .status(StatusCode::NOT_FOUND) - .body("".to_owned().into()) + .body(Body::empty()) .unwrap(); Ok(response) } diff --git a/src/http/body.rs b/src/http/body.rs index 57d1d09..bc54241 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -8,9 +8,10 @@ use crate::runtime::{AsyncPollable, Reactor, WaitFor}; pub use ::http_body::{Body as HttpBody, Frame, SizeHint}; pub use bytes::Bytes; +// FIXME can we use crate::http::Error throughout here?? use anyhow::{Context as _, Error}; use http::header::CONTENT_LENGTH; -use http_body_util::combinators::BoxBody; +use http_body_util::{combinators::BoxBody, BodyExt}; use std::fmt; use std::future::{poll_fn, Future}; use std::pin::{pin, Pin}; @@ -28,6 +29,7 @@ pub struct Body(pub(crate) BodyInner); pub(crate) enum BodyInner { Boxed(BoxBody), Incoming(Incoming), + Complete(Bytes), } impl Body { @@ -92,8 +94,103 @@ impl Body { } } } + BodyInner::Complete(bytes) => { + let mut out_stream = AsyncOutputStream::new( + outgoing_body + .write() + .expect("outgoing body already written"), + ); + out_stream.write_all(&bytes).await?; + drop(out_stream); + WasiOutgoingBody::finish(outgoing_body, None).context("finishing outgoing body")?; + Ok(()) + } + } + } + + pub fn as_boxed_body(&mut self) -> &mut BoxBody { + let mut prev = Self::empty(); + std::mem::swap(self, &mut prev); + match prev.0 { + BodyInner::Incoming(i) => self.0 = BodyInner::Boxed(i.into_http_body().boxed()), + BodyInner::Complete(bytes) => { + self.0 = BodyInner::Boxed( + http_body_util::Full::new(bytes) + .map_err(annotate_err) + .boxed(), + ) + } + BodyInner::Boxed(b) => self.0 = BodyInner::Boxed(b), + } + + match &mut self.0 { + BodyInner::Boxed(ref mut b) => b, + _ => unreachable!(), + } + } + + pub async fn contents(&mut self) -> Result<&[u8], Error> { + match &mut self.0 { + BodyInner::Complete(ref bs) => Ok(bs.as_ref()), + inner => { + let mut prev = BodyInner::Complete(Bytes::new()); + std::mem::swap(inner, &mut prev); + let boxed_body = match prev { + BodyInner::Incoming(i) => i.into_http_body().boxed(), + BodyInner::Boxed(b) => b, + BodyInner::Complete(_) => unreachable!(), + }; + let collected = boxed_body.collect().await?; + *inner = BodyInner::Complete(collected.to_bytes()); + Ok(match inner { + BodyInner::Complete(ref bs) => bs.as_ref(), + _ => unreachable!(), + }) + } } } + + pub fn content_length(&self) -> Option { + match &self.0 { + BodyInner::Boxed(b) => b.size_hint().exact(), + BodyInner::Complete(bs) => Some(bs.len() as u64), + BodyInner::Incoming(i) => i.size_hint.content_length(), + } + } + + pub fn empty() -> Self { + Body(BodyInner::Complete(Bytes::new())) + } + + pub fn from_string(s: impl Into) -> Self { + let s = s.into(); + Body(BodyInner::Complete(Bytes::from_owner(s.into_bytes()))) + } + + pub async fn str_contents(&mut self) -> Result<&str, Error> { + let bs = self.contents().await?; + Ok(std::str::from_utf8(bs)?) + } + + pub fn from_bytes(b: impl Into) -> Self { + let b = b.into(); + Body::from(http_body_util::Full::new(b)) + } + + #[cfg(feature = "json")] + pub fn from_json(data: &T) -> Result { + Ok(Self::from_string(serde_json::to_string(data)?)) + } + + #[cfg(feature = "json")] + pub async fn json serde::Deserialize<'a>>(&mut self) -> Result { + let str = self.str_contents().await?; + Ok(serde_json::from_str(str)?) + } +} + +fn annotate_err(_: E) -> anyhow::Error { + unreachable!() } impl From for Body @@ -113,24 +210,13 @@ where } } -// TODO: -// We can fill out a bunch of convenient From impls for Body - From<&str>, -// From, From<&[u8]>, From>, From, From<()>, etc etc - impl From for Body { fn from(incoming: Incoming) -> Body { Body(BodyInner::Incoming(incoming)) } } -impl Body { - pub(crate) fn content_length(&self) -> Option { - match &self.0 { - BodyInner::Boxed(b) => b.size_hint().exact(), - BodyInner::Incoming(i) => i.size_hint.content_length(), - } - } -} +impl Body {} pub struct Incoming { body: WasiIncomingBody, diff --git a/src/http/client.rs b/src/http/client.rs index 54bfeea..582cddb 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -1,4 +1,4 @@ -use super::{body::Incoming, Body, Error, Request, Response, Result}; +use super::{body::Incoming, Body, Error, Request, Response}; use crate::http::request::try_into_outgoing; use crate::http::response::try_from_incoming; use crate::io::AsyncPollable; @@ -25,25 +25,36 @@ impl Client { } /// Send an HTTP request. - /// - /// To respond with trailers, use [`Client::start_request`] instead. - pub async fn send>(&self, req: Request) -> Result> { - let (wasi_req, _body) = try_into_outgoing(req)?; - let _wasi_body = wasi_req.body().unwrap(); + pub async fn send>( + &self, + req: Request, + // FIXME change this anyhow::Error to crate::http::Error + ) -> Result, anyhow::Error> { + let (wasi_req, body) = try_into_outgoing(req)?; + let body = body.into(); + let wasi_body = wasi_req.body().unwrap(); // 1. Start sending the request head let res = wasip2::http::outgoing_handler::handle(wasi_req, self.wasi_options()?).unwrap(); - // FIXME send body into wasi_body here + let ((), body) = futures_lite::future::try_zip( + async move { + // 3. send the body: + body.send(wasi_body).await + }, + async move { + // 4. Receive the response + AsyncPollable::new(res.subscribe()).wait_for().await; - // 4. Receive the response - AsyncPollable::new(res.subscribe()).wait_for().await; - - // NOTE: the first `unwrap` is to ensure readiness, the second `unwrap` - // is to trap if we try and get the response more than once. The final - // `?` is to raise the actual error if there is one. - let res = res.get().unwrap().unwrap()?; - try_from_incoming(res) + // NOTE: the first `unwrap` is to ensure readiness, the second `unwrap` + // is to trap if we try and get the response more than once. The final + // `?` is to raise the actual error if there is one. + let res = res.get().unwrap().unwrap()?; + Ok(try_from_incoming(res)?) + }, + ) + .await?; + Ok(body) } /// Set timeout on connecting to HTTP server @@ -71,7 +82,7 @@ impl Client { } } - fn wasi_options(&self) -> Result> { + fn wasi_options(&self) -> Result, crate::http::Error> { self.options .as_ref() .map(RequestOptions::to_wasi) @@ -87,7 +98,7 @@ struct RequestOptions { } impl RequestOptions { - fn to_wasi(&self) -> Result { + fn to_wasi(&self) -> Result { let wasi = WasiRequestOptions::new(); if let Some(timeout) = self.connect_timeout { wasi.set_connect_timeout(Some(timeout.0)).map_err(|()| { diff --git a/tests/http_first_byte_timeout.rs b/tests/http_first_byte_timeout.rs index f8a0ac3..56fcd65 100644 --- a/tests/http_first_byte_timeout.rs +++ b/tests/http_first_byte_timeout.rs @@ -1,8 +1,7 @@ use wstd::http::{ error::{ErrorVariant, WasiHttpErrorCode}, - Client, Request, + Body, Client, Request, }; -use wstd::io::empty; #[wstd::main] async fn main() -> Result<(), Box> { @@ -11,7 +10,7 @@ async fn main() -> Result<(), Box> { client.set_first_byte_timeout(std::time::Duration::from_millis(500)); // This get request will connect to the server, which will then wait 1 second before // returning a response. - let request = Request::get("https://postman-echo.com/delay/1").body(empty())?; + let request = Request::get("https://postman-echo.com/delay/1").body(Body::empty())?; let result = client.send(request).await; assert!(result.is_err(), "response should be an error"); From 1e149683ea56cf4b90d7a09dd12fccaaae4a5cc5 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 22 Sep 2025 10:04:24 -0700 Subject: [PATCH 09/13] fix most of the client tests/examples --- examples/complex_http_client.rs | 6 ++---- examples/http_client.rs | 16 ++++++---------- src/http/body.rs | 13 +++++++++++-- src/http/mod.rs | 2 +- tests/http_first_byte_timeout.rs | 6 +++--- tests/http_get.rs | 32 +++++++++++++++----------------- tests/http_get_json.rs | 11 +++++------ tests/http_post.rs | 25 ++++++++++++------------- tests/http_post_json.rs | 13 +++++++------ tests/http_timeout.rs | 5 ++--- 10 files changed, 64 insertions(+), 65 deletions(-) diff --git a/examples/complex_http_client.rs b/examples/complex_http_client.rs index 8a7b2b6..35532fb 100644 --- a/examples/complex_http_client.rs +++ b/examples/complex_http_client.rs @@ -1,9 +1,7 @@ use anyhow::{anyhow, Result}; use clap::{ArgAction, Parser}; use std::str::FromStr; -use wstd::http::{ - body::BodyForthcoming, Client, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri, -}; +use wstd::http::{Body, Client, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri}; /// Complex HTTP client /// @@ -96,7 +94,7 @@ async fn main() -> Result<()> { eprintln!("> {key}: {value}"); } - let (mut outgoing_body, response) = client.start_request(request).await?; + let response = client.send(request).await?; if args.body { wstd::io::copy(wstd::io::stdin(), &mut outgoing_body).await?; diff --git a/examples/http_client.rs b/examples/http_client.rs index 12bc685..e9291db 100644 --- a/examples/http_client.rs +++ b/examples/http_client.rs @@ -1,10 +1,6 @@ use anyhow::{anyhow, Result}; use clap::{ArgAction, Parser}; -use wstd::http::{ - body::{IncomingBody, StreamedBody}, - request::Builder, - Body, Client, Method, Request, Response, Uri, -}; +use wstd::http::{request::Builder, Body, Client, Incoming, Method, Request, Response, Uri}; /// Simple HTTP client /// @@ -75,11 +71,11 @@ async fn main() -> Result<()> { // Send the request. - async fn send_request( + async fn send_request( client: &Client, request: Builder, - body: B, - ) -> Result> { + body: Body, + ) -> Result> { let request = request.body(body)?; eprintln!("> {} / {:?}", request.method(), request.version()); @@ -93,7 +89,7 @@ async fn main() -> Result<()> { let response = if args.body { send_request(&client, request, StreamedBody::new(wstd::io::stdin())).await } else { - send_request(&client, request, wstd::io::empty()).await + send_request(&client, request, Body::empty()).await }?; // Print the response. @@ -104,7 +100,7 @@ async fn main() -> Result<()> { eprintln!("< {key}: {value}"); } - let mut body = response.into_body(); + let body = response.into_body().into_inner(); wstd::io::copy(&mut body, wstd::io::stdout()).await?; let trailers = body.finish().await?; diff --git a/src/http/body.rs b/src/http/body.rs index bc54241..4b3a232 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -25,7 +25,10 @@ pub mod util { pub use http_body_util::*; } +#[derive(Debug)] pub struct Body(pub(crate) BodyInner); + +#[derive(Debug)] pub(crate) enum BodyInner { Boxed(BoxBody), Incoming(Incoming), @@ -216,8 +219,7 @@ impl From for Body { } } -impl Body {} - +#[derive(Debug)] pub struct Incoming { body: WasiIncomingBody, size_hint: BodyHint, @@ -231,6 +233,9 @@ impl Incoming { pub fn into_http_body(self) -> IncomingBody { IncomingBody::new(self.body, self.size_hint) } + pub fn into_body(self) -> Body { + self.into() + } pub fn into_inner(self) -> WasiIncomingBody { self.body } @@ -270,6 +275,7 @@ impl fmt::Display for InvalidContentLength { } impl std::error::Error for InvalidContentLength {} +#[derive(Debug)] pub struct IncomingBody { state: Option>>, size_hint: BodyHint, @@ -350,6 +356,7 @@ impl HttpBody for IncomingBody { pin_project_lite::pin_project! { #[project = IBSProj] + #[derive(Debug)] enum IncomingBodyState { Body { #[pin] @@ -365,6 +372,7 @@ pin_project_lite::pin_project! { } } +#[derive(Debug)] struct BodyState { wait: Option>>, subscription: Option, @@ -418,6 +426,7 @@ impl BodyState { } } +#[derive(Debug)] struct TrailersState { wait: Option>>, subscription: Option, diff --git a/src/http/mod.rs b/src/http/mod.rs index 35ae479..e53e038 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -4,7 +4,7 @@ pub use http::status::StatusCode; pub use http::uri::{Authority, PathAndQuery, Uri}; #[doc(inline)] -pub use body::{util::BodyExt, Body, Incoming as IncomingBody}; +pub use body::{util::BodyExt, Body, Incoming}; pub use client::Client; pub use error::{Error, Result}; pub use fields::{HeaderMap, HeaderName, HeaderValue}; diff --git a/tests/http_first_byte_timeout.rs b/tests/http_first_byte_timeout.rs index 56fcd65..ec15248 100644 --- a/tests/http_first_byte_timeout.rs +++ b/tests/http_first_byte_timeout.rs @@ -1,5 +1,5 @@ use wstd::http::{ - error::{ErrorVariant, WasiHttpErrorCode}, + error::{Error, ErrorCode, ErrorVariant}, Body, Client, Request, }; @@ -17,8 +17,8 @@ async fn main() -> Result<(), Box> { let error = result.unwrap_err(); assert!( matches!( - error.variant(), - ErrorVariant::WasiHttp(WasiHttpErrorCode::ConnectionReadTimeout) + error.downcast_ref::().map(|e| e.variant()), + Some(ErrorVariant::WasiHttp(ErrorCode::ConnectionReadTimeout)) ), "expected ConnectionReadTimeout error, got: {error:?>}" ); diff --git a/tests/http_get.rs b/tests/http_get.rs index 9100237..4f4bc33 100644 --- a/tests/http_get.rs +++ b/tests/http_get.rs @@ -1,41 +1,39 @@ use std::error::Error; use wstd::http::{Body, Client, HeaderValue, Request}; -use wstd::io::{empty, AsyncRead}; #[wstd::test] async fn main() -> Result<(), Box> { let request = Request::get("https://postman-echo.com/get") .header("my-header", HeaderValue::from_str("my-value")?) - .body(empty())?; + .body(Body::empty())?; - let mut response = Client::new().send(request).await?; + let response = Client::new().send(request).await?; let content_type = response .headers() .get("Content-Type") - .ok_or_else(|| "response expected to have Content-Type header")?; + .ok_or("response expected to have Content-Type header")?; assert_eq!(content_type, "application/json; charset=utf-8"); - let body = response.body_mut(); + let mut body = response.into_body().into_body(); let body_len = body - .len() - .ok_or_else(|| "GET postman-echo.com/get is supposed to provide a content-length")?; + .content_length() + .ok_or("GET postman-echo.com/get is supposed to provide a content-length")?; - let mut body_buf = Vec::new(); - body.read_to_end(&mut body_buf).await?; + let contents = body.contents().await?; assert_eq!( - body_buf.len(), + contents.len() as u64, body_len, - "read_to_end length should match content-length" + "contents length should match content-length" ); - let val: serde_json::Value = serde_json::from_slice(&body_buf)?; + let val: serde_json::Value = serde_json::from_slice(contents)?; let body_url = val .get("url") - .ok_or_else(|| "body json has url")? + .ok_or("body json has url")? .as_str() - .ok_or_else(|| "body json url is str")?; + .ok_or("body json url is str")?; assert!( body_url.contains("postman-echo.com/get"), "expected body url to contain the authority and path, got: {body_url}" @@ -43,11 +41,11 @@ async fn main() -> Result<(), Box> { assert_eq!( val.get("headers") - .ok_or_else(|| "body json has headers")? + .ok_or("body json has headers")? .get("my-header") - .ok_or_else(|| "headers contains my-header")? + .ok_or("headers contains my-header")? .as_str() - .ok_or_else(|| "my-header is a str")?, + .ok_or("my-header is a str")?, "my-value" ); diff --git a/tests/http_get_json.rs b/tests/http_get_json.rs index de409d3..15548db 100644 --- a/tests/http_get_json.rs +++ b/tests/http_get_json.rs @@ -1,7 +1,6 @@ use serde::Deserialize; use std::error::Error; -use wstd::http::{Client, Request}; -use wstd::io::empty; +use wstd::http::{Body, Client, Request}; #[derive(Deserialize)] struct Echo { @@ -10,17 +9,17 @@ struct Echo { #[wstd::test] async fn main() -> Result<(), Box> { - let request = Request::get("https://postman-echo.com/get").body(empty())?; + let request = Request::get("https://postman-echo.com/get").body(Body::empty())?; - let mut response = Client::new().send(request).await?; + let response = Client::new().send(request).await?; let content_type = response .headers() .get("Content-Type") - .ok_or_else(|| "response expected to have Content-Type header")?; + .ok_or("response expected to have Content-Type header")?; assert_eq!(content_type, "application/json; charset=utf-8"); - let Echo { url } = response.body_mut().json::().await?; + let Echo { url } = response.into_body().into_body().json::().await?; assert!( url.contains("postman-echo.com/get"), "expected body url to contain the authority and path, got: {url}" diff --git a/tests/http_post.rs b/tests/http_post.rs index c2257a4..3926e60 100644 --- a/tests/http_post.rs +++ b/tests/http_post.rs @@ -1,6 +1,5 @@ use std::error::Error; -use wstd::http::{Client, HeaderValue, IntoBody, Request}; -use wstd::io::AsyncRead; +use wstd::http::{Body, Client, HeaderValue, Request}; #[wstd::test] async fn main() -> Result<(), Box> { @@ -9,25 +8,25 @@ async fn main() -> Result<(), Box> { "content-type", HeaderValue::from_str("application/json; charset=utf-8")?, ) - .body("{\"test\": \"data\"}".into_body())?; + .body(Body::from_string("{\"test\": \"data\"}"))?; - let mut response = Client::new().send(request).await?; + let response = Client::new().send(request).await?; let content_type = response .headers() .get("Content-Type") - .ok_or_else(|| "response expected to have Content-Type header")?; + .ok_or("response expected to have Content-Type header")?; assert_eq!(content_type, "application/json; charset=utf-8"); - let mut body_buf = Vec::new(); - response.body_mut().read_to_end(&mut body_buf).await?; + let mut body = response.into_body().into_body(); + let body_buf = body.contents().await?; - let val: serde_json::Value = serde_json::from_slice(&body_buf)?; + let val: serde_json::Value = serde_json::from_slice(body_buf)?; let body_url = val .get("url") - .ok_or_else(|| "body json has url")? + .ok_or("body json has url")? .as_str() - .ok_or_else(|| "body json url is str")?; + .ok_or("body json url is str")?; assert!( body_url.contains("postman-echo.com/post"), "expected body url to contain the authority and path, got: {body_url}" @@ -35,7 +34,7 @@ async fn main() -> Result<(), Box> { let posted_json = val .get("json") - .ok_or_else(|| "body json has 'json' key")? + .ok_or("body json has 'json' key")? .as_object() .ok_or_else(|| format!("body json 'json' is object. got {val:?}"))?; @@ -43,9 +42,9 @@ async fn main() -> Result<(), Box> { assert_eq!( posted_json .get("test") - .ok_or_else(|| "returned json has 'test' key")? + .ok_or("returned json has 'test' key")? .as_str() - .ok_or_else(|| "returned json 'test' key should be str value")?, + .ok_or("returned json 'test' key should be str value")?, "data" ); diff --git a/tests/http_post_json.rs b/tests/http_post_json.rs index 5ccb0cc..ba961a0 100644 --- a/tests/http_post_json.rs +++ b/tests/http_post_json.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use std::error::Error; -use wstd::http::{request::JsonRequest, Client, Request}; +use wstd::http::{Body, Client, Request}; #[derive(Serialize)] struct TestData { @@ -17,23 +17,24 @@ async fn main() -> Result<(), Box> { let test_data = TestData { test: "data".to_string(), }; - let request = Request::post("https://postman-echo.com/post").json(&test_data)?; + let request = + Request::post("https://postman-echo.com/post").body(Body::from_json(&test_data)?)?; let content_type = request .headers() .get("Content-Type") - .ok_or_else(|| "request expected to have Content-Type header")?; + .ok_or("request expected to have Content-Type header")?; assert_eq!(content_type, "application/json; charset=utf-8"); - let mut response = Client::new().send(request).await?; + let response = Client::new().send(request).await?; let content_type = response .headers() .get("Content-Type") - .ok_or_else(|| "response expected to have Content-Type header")?; + .ok_or("response expected to have Content-Type header")?; assert_eq!(content_type, "application/json; charset=utf-8"); - let Echo { url } = response.body_mut().json::().await?; + let Echo { url } = response.into_body().into_body().json::().await?; assert!( url.contains("postman-echo.com/post"), "expected body url to contain the authority and path, got: {url}" diff --git a/tests/http_timeout.rs b/tests/http_timeout.rs index dea1ac9..96d40de 100644 --- a/tests/http_timeout.rs +++ b/tests/http_timeout.rs @@ -1,13 +1,12 @@ use wstd::future::FutureExt; -use wstd::http::{Client, Request}; -use wstd::io::empty; +use wstd::http::{Body, Client, Request}; use wstd::time::Duration; #[wstd::test] async fn http_timeout() -> Result<(), Box> { // This get request will connect to the server, which will then wait 1 second before // returning a response. - let request = Request::get("https://postman-echo.com/delay/1").body(empty())?; + let request = Request::get("https://postman-echo.com/delay/1").body(Body::empty())?; let result = Client::new() .send(request) .timeout(Duration::from_millis(500)) From 7283a73cf8babb2cd651687a23b0c5634e0484e9 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 22 Sep 2025 11:01:43 -0700 Subject: [PATCH 10/13] a couple more fixes - no need to spawn in responder --- macro/src/lib.rs | 6 +++--- src/http/server.rs | 19 ++++++------------- tests/http_post_json.rs | 13 ++++++------- 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/macro/src/lib.rs b/macro/src/lib.rs index b13cff0..956613f 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -138,10 +138,10 @@ pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStr ::wstd::runtime::block_on(async move { match ::wstd::http::request::try_from_incoming(request) { Ok(request) => match __run(request).await { - Ok(response) => responder.respond(response).await, - Err(err) => responder.fail(err), + Ok(response) => { responder.respond(response).await.unwrap() }, + Err(err) => responder.fail(err).unwrap(), } - Err(err) => responder.fail(err), + Err(err) => responder.fail(err).unwrap(), } }) } diff --git a/src/http/server.rs b/src/http/server.rs index db8b420..664f8be 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -21,6 +21,7 @@ //! [`http_server`]: crate::http_server use super::{error::ErrorCode, fields::header_map_to_wasi, Body, Response}; +use anyhow::Error; use http::header::CONTENT_LENGTH; use wasip2::exports::http::incoming_handler::ResponseOutparam; use wasip2::http::types::OutgoingResponse; @@ -57,7 +58,7 @@ impl Responder { /// # } /// # fn main() {} /// ``` - pub async fn respond>(self, response: Response) { + pub async fn respond>(self, response: Response) -> Result<(), Error> { let headers = response.headers(); let status = response.status().as_u16(); @@ -85,16 +86,7 @@ impl Responder { // Tell WASI to start the show. ResponseOutparam::set(self.outparam, Ok(wasi_response)); - crate::runtime::spawn(async move { - if let Err(e) = body.send(wasi_body).await { - // FIXME we have no way to actually report this, which kinda - // sucks. we can make this function return a Task and have the - // macro detatch and eprintln it to at least make better - // things possible in a better embedding?? - eprintln!("Outgoing response body error: {e:?}") - } - }) - .detach(); + body.send(wasi_body).await } /// This is used by the `http_server` macro. @@ -105,7 +97,8 @@ impl Responder { /// This is used by the `http_server` macro. #[doc(hidden)] - pub fn fail(self, err: ErrorCode) { - ResponseOutparam::set(self.outparam, Err(err)) + pub fn fail(self, err: ErrorCode) -> Result<(), Error> { + ResponseOutparam::set(self.outparam, Err(err.clone())); + Err(err.into()) } } diff --git a/tests/http_post_json.rs b/tests/http_post_json.rs index ba961a0..6a3e5ae 100644 --- a/tests/http_post_json.rs +++ b/tests/http_post_json.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use std::error::Error; -use wstd::http::{Body, Client, Request}; +use wstd::http::{Body, Client, HeaderValue, Request}; #[derive(Serialize)] struct TestData { @@ -17,14 +17,13 @@ async fn main() -> Result<(), Box> { let test_data = TestData { test: "data".to_string(), }; - let request = + let mut request = Request::post("https://postman-echo.com/post").body(Body::from_json(&test_data)?)?; - let content_type = request - .headers() - .get("Content-Type") - .ok_or("request expected to have Content-Type header")?; - assert_eq!(content_type, "application/json; charset=utf-8"); + request.headers_mut().insert( + "Content-Type", + HeaderValue::from_static("application/json; charset=utf-8"), + ); let response = Client::new().send(request).await?; From 4545d319f42a88f0d4c2cd364081ff2f9124b51f Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 23 Sep 2025 13:45:01 -0700 Subject: [PATCH 11/13] translate example/http_client to new api --- examples/http_client.rs | 18 ++++++++++++------ src/http/body.rs | 10 ++++++++++ src/io/stdio.rs | 15 +++++++++++++++ src/io/streams.rs | 26 +++++++++++++------------- 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/examples/http_client.rs b/examples/http_client.rs index e9291db..3ef695a 100644 --- a/examples/http_client.rs +++ b/examples/http_client.rs @@ -1,6 +1,9 @@ use anyhow::{anyhow, Result}; use clap::{ArgAction, Parser}; -use wstd::http::{request::Builder, Body, Client, Incoming, Method, Request, Response, Uri}; +use wstd::http::{ + request::Builder, Body, BodyExt, Client, Incoming, Method, Request, Response, Uri, +}; +use wstd::io::AsyncWrite; /// Simple HTTP client /// @@ -84,10 +87,11 @@ async fn main() -> Result<()> { eprintln!("> {key}: {value}"); } - Ok(client.send(request).await?) + client.send(request).await } let response = if args.body { - send_request(&client, request, StreamedBody::new(wstd::io::stdin())).await + let body = Body::from_input_stream(wstd::io::stdin().into_inner()); + send_request(&client, request, body).await } else { send_request(&client, request, Body::empty()).await }?; @@ -100,10 +104,12 @@ async fn main() -> Result<()> { eprintln!("< {key}: {value}"); } - let body = response.into_body().into_inner(); - wstd::io::copy(&mut body, wstd::io::stdout()).await?; + let body = response.into_body().into_http_body().collect().await?; + let trailers = body.trailers().cloned(); + wstd::io::stdout() + .write_all(body.to_bytes().as_ref()) + .await?; - let trailers = body.finish().await?; if let Some(trailers) = trailers { for (key, value) in trailers.iter() { let value = String::from_utf8_lossy(value.as_bytes()); diff --git a/src/http/body.rs b/src/http/body.rs index 4b3a232..66a8dbb 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -190,6 +190,16 @@ impl Body { let str = self.str_contents().await?; Ok(serde_json::from_str(str)?) } + + pub fn from_input_stream(r: crate::io::AsyncInputStream) -> Self { + use futures_lite::stream::StreamExt; + Body(BodyInner::Boxed(http_body_util::BodyExt::boxed( + http_body_util::StreamBody::new(r.into_stream().map(|res| { + res.map(|bytevec| Frame::data(Bytes::from_owner(bytevec))) + .map_err(Into::into) + })), + ))) + } } fn annotate_err(_: E) -> anyhow::Error { diff --git a/src/io/stdio.rs b/src/io/stdio.rs index 0b9707d..af8a0ae 100644 --- a/src/io/stdio.rs +++ b/src/io/stdio.rs @@ -24,6 +24,11 @@ impl Stdin { pub fn is_terminal(&self) -> bool { LazyCell::force(&self.terminput).is_some() } + + /// Get the `AsyncInputStream` used to implement `Stdin` + pub fn into_inner(self) -> AsyncInputStream { + self.stream + } } #[async_trait::async_trait(?Send)] @@ -65,6 +70,11 @@ impl Stdout { pub fn is_terminal(&self) -> bool { LazyCell::force(&self.termoutput).is_some() } + + /// Get the `AsyncOutputStream` used to implement `Stdout` + pub fn into_inner(self) -> AsyncOutputStream { + self.stream + } } #[async_trait::async_trait(?Send)] @@ -111,6 +121,11 @@ impl Stderr { pub fn is_terminal(&self) -> bool { LazyCell::force(&self.termoutput).is_some() } + + /// Get the `AsyncOutputStream` used to implement `Stderr` + pub fn into_inner(self) -> AsyncOutputStream { + self.stream + } } #[async_trait::async_trait(?Send)] diff --git a/src/io/streams.rs b/src/io/streams.rs index f3cd6fa..fd97063 100644 --- a/src/io/streams.rs +++ b/src/io/streams.rs @@ -1,7 +1,7 @@ use super::{AsyncPollable, AsyncRead, AsyncWrite}; -use std::cell::OnceCell; use std::future::{poll_fn, Future}; use std::pin::Pin; +use std::sync::OnceLock; use std::task::{Context, Poll}; use wasip2::io::streams::{InputStream, OutputStream, StreamError}; @@ -11,7 +11,7 @@ use wasip2::io::streams::{InputStream, OutputStream, StreamError}; pub struct AsyncInputStream { // Lazily initialized pollable, used for lifetime of stream to check readiness. // Field ordering matters: this child must be dropped before stream - subscription: OnceCell, + subscription: OnceLock, stream: InputStream, } @@ -19,7 +19,7 @@ impl AsyncInputStream { /// Construct an `AsyncInputStream` from a WASI `InputStream` resource. pub fn new(stream: InputStream) -> Self { Self { - subscription: OnceCell::new(), + subscription: OnceLock::new(), stream, } } @@ -64,7 +64,7 @@ impl AsyncInputStream { Ok(len) } - /// Use this `AsyncInputStream` as a `futures_core::stream::Stream` with + /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with /// items of `Result, std::io::Error>`. The returned byte vectors /// will be at most 8k. If you want to control chunk size, use /// `Self::into_stream_of`. @@ -75,7 +75,7 @@ impl AsyncInputStream { } } - /// Use this `AsyncInputStream` as a `futures_core::stream::Stream` with + /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with /// items of `Result, std::io::Error>`. The returned byte vectors /// will be at most the `chunk_size` argument specified. pub fn into_stream_of(self, chunk_size: usize) -> AsyncInputChunkStream { @@ -85,7 +85,7 @@ impl AsyncInputStream { } } - /// Use this `AsyncInputStream` as a `futures_core::stream::Stream` with + /// Use this `AsyncInputStream` as a `futures_lite::stream::Stream` with /// items of `Result`. pub fn into_bytestream(self) -> AsyncInputByteStream { AsyncInputByteStream { @@ -107,7 +107,7 @@ impl AsyncRead for AsyncInputStream { } } -/// Wrapper of `AsyncInputStream` that impls `futures_core::stream::Stream` +/// Wrapper of `AsyncInputStream` that impls `futures_lite::stream::Stream` /// with an item of `Result, std::io::Error>` pub struct AsyncInputChunkStream { stream: AsyncInputStream, @@ -121,7 +121,7 @@ impl AsyncInputChunkStream { } } -impl futures_core::stream::Stream for AsyncInputChunkStream { +impl futures_lite::stream::Stream for AsyncInputChunkStream { type Item = Result, std::io::Error>; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { match self.stream.poll_ready(cx) { @@ -140,7 +140,7 @@ impl futures_core::stream::Stream for AsyncInputChunkStream { pin_project_lite::pin_project! { /// Wrapper of `AsyncInputStream` that impls - /// `futures_core::stream::Stream` with item `Result`. + /// `futures_lite::stream::Stream` with item `Result`. pub struct AsyncInputByteStream { #[pin] stream: AsyncInputChunkStream, @@ -162,13 +162,13 @@ impl AsyncInputByteStream { } } -impl futures_core::stream::Stream for AsyncInputByteStream { +impl futures_lite::stream::Stream for AsyncInputByteStream { type Item = Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.project(); match this.buffer.next() { Some(byte) => Poll::Ready(Some(Ok(byte.expect("cursor on Vec is infallible")))), - None => match futures_core::stream::Stream::poll_next(this.stream, cx) { + None => match futures_lite::stream::Stream::poll_next(this.stream, cx) { Poll::Ready(Some(Ok(bytes))) => { let mut bytes = std::io::Read::bytes(std::io::Cursor::new(bytes)); match bytes.next() { @@ -194,7 +194,7 @@ impl futures_core::stream::Stream for AsyncInputByteStream { pub struct AsyncOutputStream { // Lazily initialized pollable, used for lifetime of stream to check readiness. // Field ordering matters: this child must be dropped before stream - subscription: OnceCell, + subscription: OnceLock, stream: OutputStream, } @@ -202,7 +202,7 @@ impl AsyncOutputStream { /// Construct an `AsyncOutputStream` from a WASI `OutputStream` resource. pub fn new(stream: OutputStream) -> Self { Self { - subscription: OnceCell::new(), + subscription: OnceLock::new(), stream, } } From 8474e1f1037f18682e034f5ed6c5db9dc1a650ab Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Tue, 23 Sep 2025 14:11:37 -0700 Subject: [PATCH 12/13] complex http client working as well --- examples/complex_http_client.rs | 39 +++++++++++++++++++-------------- examples/http_client.rs | 35 +++++++++++------------------ src/http/body.rs | 22 +++++++++---------- 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/examples/complex_http_client.rs b/examples/complex_http_client.rs index 35532fb..3754d60 100644 --- a/examples/complex_http_client.rs +++ b/examples/complex_http_client.rs @@ -1,7 +1,8 @@ use anyhow::{anyhow, Result}; use clap::{ArgAction, Parser}; use std::str::FromStr; -use wstd::http::{Body, Client, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri}; +use wstd::http::{Body, BodyExt, Client, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri}; +use wstd::io::AsyncWrite; /// Complex HTTP client /// @@ -84,10 +85,22 @@ async fn main() -> Result<()> { trailers.insert(HeaderName::from_str(key)?, HeaderValue::from_str(value)?); } - // Send the request. - - let request = request.body(BodyForthcoming)?; + let body = if args.body { + Body::from_input_stream(wstd::io::stdin().into_inner()).into_boxed_body() + } else { + Body::empty().into_boxed_body() + }; + let t = trailers.clone(); + let body = body.with_trailers(async move { + if t.is_empty() { + None + } else { + Some(Ok(t)) + } + }); + let request = request.body(body)?; + // Send the request. eprintln!("> {} / {:?}", request.method(), request.version()); for (key, value) in request.headers().iter() { let value = String::from_utf8_lossy(value.as_bytes()); @@ -96,12 +109,6 @@ async fn main() -> Result<()> { let response = client.send(request).await?; - if args.body { - wstd::io::copy(wstd::io::stdin(), &mut outgoing_body).await?; - } else { - wstd::io::copy(wstd::io::empty(), &mut outgoing_body).await?; - } - if !trailers.is_empty() { eprintln!("..."); } @@ -110,10 +117,6 @@ async fn main() -> Result<()> { eprintln!("> {key}: {value}"); } - Client::finish(outgoing_body, Some(trailers))?; - - let response = response.await?; - // Print the response. eprintln!("< {:?} {}", response.version(), response.status()); @@ -122,10 +125,12 @@ async fn main() -> Result<()> { eprintln!("< {key}: {value}"); } - let mut body = response.into_body(); - wstd::io::copy(&mut body, wstd::io::stdout()).await?; + let body = response.into_body().into_http_body().collect().await?; + let trailers = body.trailers().cloned(); + wstd::io::stdout() + .write_all(body.to_bytes().as_ref()) + .await?; - let trailers = body.finish().await?; if let Some(trailers) = trailers { for (key, value) in trailers.iter() { let value = String::from_utf8_lossy(value.as_bytes()); diff --git a/examples/http_client.rs b/examples/http_client.rs index 3ef695a..2153f41 100644 --- a/examples/http_client.rs +++ b/examples/http_client.rs @@ -1,8 +1,6 @@ use anyhow::{anyhow, Result}; use clap::{ArgAction, Parser}; -use wstd::http::{ - request::Builder, Body, BodyExt, Client, Incoming, Method, Request, Response, Uri, -}; +use wstd::http::{Body, BodyExt, Client, Method, Request, Uri}; use wstd::io::AsyncWrite; /// Simple HTTP client @@ -74,30 +72,23 @@ async fn main() -> Result<()> { // Send the request. - async fn send_request( - client: &Client, - request: Builder, - body: Body, - ) -> Result> { - let request = request.body(body)?; + let body = if args.body { + Body::from_input_stream(wstd::io::stdin().into_inner()) + } else { + Body::empty() + }; - eprintln!("> {} / {:?}", request.method(), request.version()); - for (key, value) in request.headers().iter() { - let value = String::from_utf8_lossy(value.as_bytes()); - eprintln!("> {key}: {value}"); - } + let request = request.body(body)?; - client.send(request).await + eprintln!("> {} / {:?}", request.method(), request.version()); + for (key, value) in request.headers().iter() { + let value = String::from_utf8_lossy(value.as_bytes()); + eprintln!("> {key}: {value}"); } - let response = if args.body { - let body = Body::from_input_stream(wstd::io::stdin().into_inner()); - send_request(&client, request, body).await - } else { - send_request(&client, request, Body::empty()).await - }?; - // Print the response. + let response = client.send(request).await?; + // Print the response. eprintln!("< {:?} {}", response.version(), response.status()); for (key, value) in response.headers().iter() { let value = String::from_utf8_lossy(value.as_bytes()); diff --git a/src/http/body.rs b/src/http/body.rs index 66a8dbb..c2db93c 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -111,20 +111,20 @@ impl Body { } } + pub fn into_boxed_body(self) -> BoxBody { + match self.0 { + BodyInner::Incoming(i) => i.into_http_body().boxed(), + BodyInner::Complete(bytes) => http_body_util::Full::new(bytes) + .map_err(annotate_err) + .boxed(), + BodyInner::Boxed(b) => b, + } + } + pub fn as_boxed_body(&mut self) -> &mut BoxBody { let mut prev = Self::empty(); std::mem::swap(self, &mut prev); - match prev.0 { - BodyInner::Incoming(i) => self.0 = BodyInner::Boxed(i.into_http_body().boxed()), - BodyInner::Complete(bytes) => { - self.0 = BodyInner::Boxed( - http_body_util::Full::new(bytes) - .map_err(annotate_err) - .boxed(), - ) - } - BodyInner::Boxed(b) => self.0 = BodyInner::Boxed(b), - } + self.0 = BodyInner::Boxed(prev.into_boxed_body()); match &mut self.0 { BodyInner::Boxed(ref mut b) => b, From 895d7be2fa52511963fce64702631f4f23cd888a Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 6 Oct 2025 14:57:49 -0700 Subject: [PATCH 13/13] revamp errors, fix comments, delete dead coee --- examples/http_server.rs | 8 +- src/http/body.rs | 54 +++-- src/http/body.rs.disabled | 391 ------------------------------- src/http/client.rs | 21 +- src/http/error.rs | 133 +---------- src/http/fields.rs | 32 +-- src/http/mod.rs | 2 +- src/http/request.rs | 67 +++--- src/http/response.rs | 18 +- src/http/server.rs | 59 ++--- tests/http_first_byte_timeout.rs | 9 +- 11 files changed, 121 insertions(+), 673 deletions(-) delete mode 100644 src/http/body.rs.disabled diff --git a/examples/http_server.rs b/examples/http_server.rs index 37eaadb..d4df71c 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -2,11 +2,11 @@ use anyhow::{Context, Result}; use futures_lite::stream::once_future; use http_body_util::{BodyExt, StreamBody}; use wstd::http::body::{Body, Bytes, Frame, Incoming}; -use wstd::http::{error::ErrorCode, HeaderMap, Request, Response, StatusCode}; +use wstd::http::{Error, HeaderMap, Request, Response, StatusCode}; use wstd::time::{Duration, Instant}; #[wstd::http_server] -async fn main(request: Request) -> Result, ErrorCode> { +async fn main(request: Request) -> Result, Error> { let path = request.uri().path_and_query().unwrap().as_str(); println!("serving {path}"); match path { @@ -21,10 +21,6 @@ async fn main(request: Request) -> Result, ErrorCode> { "/response-body-fail" => http_body_fail(request).await, _ => http_not_found(request).await, } - .map_err(|e| match e.downcast::() { - Ok(e) => e, - Err(e) => ErrorCode::InternalError(Some(format!("{e:?}"))), - }) } async fn http_home(_request: Request) -> Result> { diff --git a/src/http/body.rs b/src/http/body.rs index c2db93c..2e6b434 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -1,6 +1,7 @@ use crate::http::{ + error::Context as _, fields::{header_map_from_wasi, header_map_to_wasi}, - HeaderMap, + Error, HeaderMap, }; use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncWrite}; use crate::runtime::{AsyncPollable, Reactor, WaitFor}; @@ -8,8 +9,6 @@ use crate::runtime::{AsyncPollable, Reactor, WaitFor}; pub use ::http_body::{Body as HttpBody, Frame, SizeHint}; pub use bytes::Bytes; -// FIXME can we use crate::http::Error throughout here?? -use anyhow::{Context as _, Error}; use http::header::CONTENT_LENGTH; use http_body_util::{combinators::BoxBody, BodyExt}; use std::fmt; @@ -49,7 +48,10 @@ impl Body { ); crate::io::copy(&mut in_stream, &mut out_stream) .await - .context("copying incoming body stream to outgoing body stream")?; + .map_err(|e| { + Error::from(e) + .context("copying incoming body stream to outgoing body stream") + })?; drop(in_stream); drop(out_stream); let future_in_trailers = WasiIncomingBody::finish(in_body); @@ -61,9 +63,9 @@ impl Body { .get() .expect("pollable ready") .expect("got once") - .context("recieving incoming trailers")?; + .map_err(|e| Error::from(e).context("recieving incoming trailers"))?; WasiOutgoingBody::finish(outgoing_body, in_trailers) - .context("finishing outgoing body")?; + .map_err(|e| Error::from(e).context("finishing outgoing body"))?; Ok(()) } BodyInner::Boxed(box_body) => { @@ -81,16 +83,16 @@ impl Body { out_stream.write_all(data).await?; } Some(Ok(frame)) if frame.is_trailers() => { - trailers = Some( - header_map_to_wasi(frame.trailers_ref().unwrap()) - .context("outoging trailers to wasi")?, - ); + trailers = + Some(header_map_to_wasi(frame.trailers_ref().unwrap()).map_err( + |e| Error::from(e).context("outoging trailers to wasi"), + )?); } Some(Err(err)) => break Err(err.context("sending outgoing body")), None => { drop(out_stream); WasiOutgoingBody::finish(outgoing_body, trailers) - .context("finishing outgoing body")?; + .map_err(|e| Error::from(e).context("finishing outgoing body"))?; break Ok(()); } _ => unreachable!(), @@ -105,7 +107,8 @@ impl Body { ); out_stream.write_all(&bytes).await?; drop(out_stream); - WasiOutgoingBody::finish(outgoing_body, None).context("finishing outgoing body")?; + WasiOutgoingBody::finish(outgoing_body, None) + .map_err(|e| Error::from(e).context("finishing outgoing body"))?; Ok(()) } } @@ -172,7 +175,7 @@ impl Body { pub async fn str_contents(&mut self) -> Result<&str, Error> { let bs = self.contents().await?; - Ok(std::str::from_utf8(bs)?) + std::str::from_utf8(bs).context("decoding body contents as string") } pub fn from_bytes(b: impl Into) -> Self { @@ -188,7 +191,7 @@ impl Body { #[cfg(feature = "json")] pub async fn json serde::Deserialize<'a>>(&mut self) -> Result { let str = self.str_contents().await?; - Ok(serde_json::from_str(str)?) + serde_json::from_str(str).context("decoding body contents as json") } pub fn from_input_stream(r: crate::io::AsyncInputStream) -> Self { @@ -202,7 +205,7 @@ impl Body { } } -fn annotate_err(_: E) -> anyhow::Error { +fn annotate_err(_: E) -> Error { unreachable!() } @@ -311,7 +314,7 @@ impl IncomingBody { impl HttpBody for IncomingBody { type Data = Bytes; - type Error = anyhow::Error; + type Error = Error; fn poll_frame( mut self: Pin<&mut Self>, cx: &mut Context<'_>, @@ -395,7 +398,7 @@ impl BodyState { fn poll_frame( mut self: Pin<&mut Self>, cx: &mut Context<'_>, - ) -> Poll, anyhow::Error>>> { + ) -> Poll, Error>>> { loop { match self.stream.read(MAX_FRAME_SIZE) { Ok(bs) if !bs.is_empty() => { @@ -403,10 +406,9 @@ impl BodyState { } Err(StreamError::Closed) => return Poll::Ready(None), Err(StreamError::LastOperationFailed(err)) => { - return Poll::Ready(Some(Err(anyhow::anyhow!( - "error reading incoming body: {}", - err.to_debug_string() - )))) + return Poll::Ready(Some(Err( + Error::msg(err.to_debug_string()).context("reading incoming body stream") + ))) } Ok(_empty) => { if self.subscription.is_none() { @@ -455,19 +457,19 @@ impl TrailersState { fn poll_frame( mut self: Pin<&mut Self>, cx: &mut Context<'_>, - ) -> Poll, anyhow::Error>>> { + ) -> Poll, Error>>> { loop { if let Some(ready) = self.future_trailers.get() { return match ready { Ok(Ok(Some(trailers))) => match header_map_from_wasi(trailers) { Ok(header_map) => Poll::Ready(Some(Ok(Frame::trailers(header_map)))), - Err(e) => Poll::Ready(Some(Err(e - .context("decoding incoming body trailers") - .into()))), + Err(e) => { + Poll::Ready(Some(Err(e.context("decoding incoming body trailers")))) + } }, Ok(Ok(None)) => Poll::Ready(None), Ok(Err(e)) => Poll::Ready(Some(Err( - anyhow::Error::from(e).context("recieving incoming body trailers") + Error::from(e).context("reading incoming body trailers") ))), Err(()) => unreachable!("future_trailers.get with some called at most once"), }; diff --git a/src/http/body.rs.disabled b/src/http/body.rs.disabled deleted file mode 100644 index bd6f65b..0000000 --- a/src/http/body.rs.disabled +++ /dev/null @@ -1,391 +0,0 @@ -//! HTTP body types - -use crate::http::fields::header_map_from_wasi; -use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Cursor, Empty}; -use crate::runtime::AsyncPollable; -use core::fmt; -use http::header::CONTENT_LENGTH; -use wasi::http::types::IncomingBody as WasiIncomingBody; - -#[cfg(feature = "json")] -use serde::de::DeserializeOwned; -#[cfg(feature = "json")] -use serde_json; - -pub use super::{ - error::{Error, ErrorVariant}, - HeaderMap, -}; - -#[derive(Debug)] -pub(crate) enum BodyKind { - Fixed(u64), - Chunked, -} - -impl BodyKind { - pub(crate) fn from_headers(headers: &HeaderMap) -> Result { - if let Some(value) = headers.get(CONTENT_LENGTH) { - let content_length = std::str::from_utf8(value.as_ref()) - .unwrap() - .parse::() - .map_err(|_| InvalidContentLength)?; - Ok(BodyKind::Fixed(content_length)) - } else { - Ok(BodyKind::Chunked) - } - } -} - -/// A trait representing an HTTP body. -pub trait Body: AsyncRead { - /// Returns the exact remaining length of the iterator, if known. - fn len(&self) -> Option; - - /// Returns `true` if the body is known to be empty. - fn is_empty(&self) -> bool { - matches!(self.len(), Some(0)) - } -} - -/// A boxed trait object for a `Body`. -pub struct BoxBody(pub Box); -impl BoxBody { - fn new(body: impl Body + 'static) -> Self { - BoxBody(Box::new(body)) - } -} -#[async_trait::async_trait(?Send)] -impl AsyncRead for BoxBody { - async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { - self.0.read(buf).await - } -} -impl Body for BoxBody { - /// Returns the exact remaining length of the iterator, if known. - fn len(&self) -> Option { - self.0.len() - } - - /// Returns `true` if the body is known to be empty. - fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -/// Conversion into a `Body`. -#[doc(hidden)] -pub trait IntoBody { - /// What type of `Body` are we turning this into? - type IntoBody: Body; - /// Convert into `Body`. - fn into_body(self) -> Self::IntoBody; - - /// Convert into `BoxBody`. - fn into_boxed_body(self) -> BoxBody - where - Self: Sized, - Self::IntoBody: 'static, - { - BoxBody::new(self.into_body()) - } -} -impl IntoBody for T -where - T: Body, -{ - type IntoBody = T; - fn into_body(self) -> Self::IntoBody { - self - } -} - -impl IntoBody for String { - type IntoBody = BoundedBody>; - fn into_body(self) -> Self::IntoBody { - BoundedBody(Cursor::new(self.into_bytes())) - } -} - -impl IntoBody for &str { - type IntoBody = BoundedBody>; - fn into_body(self) -> Self::IntoBody { - BoundedBody(Cursor::new(self.to_owned().into_bytes())) - } -} - -impl IntoBody for Vec { - type IntoBody = BoundedBody>; - fn into_body(self) -> Self::IntoBody { - BoundedBody(Cursor::new(self)) - } -} - -impl IntoBody for &[u8] { - type IntoBody = BoundedBody>; - fn into_body(self) -> Self::IntoBody { - BoundedBody(Cursor::new(self.to_owned())) - } -} - -/// An HTTP body with a known length -#[derive(Debug)] -pub struct BoundedBody(Cursor); - -#[async_trait::async_trait(?Send)] -impl> AsyncRead for BoundedBody { - async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { - self.0.read(buf).await - } -} -impl> Body for BoundedBody { - fn len(&self) -> Option { - Some(self.0.get_ref().as_ref().len()) - } -} - -/// An HTTP body with an unknown length -#[derive(Debug)] -pub struct StreamedBody(S); - -impl StreamedBody { - /// Wrap an `AsyncRead` impl in a type that provides a [`Body`] implementation. - pub fn new(s: S) -> Self { - Self(s) - } -} - -#[async_trait::async_trait(?Send)] -impl AsyncRead for StreamedBody { - async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { - self.0.read(buf).await - } -} -impl Body for StreamedBody { - fn len(&self) -> Option { - None - } -} - -impl Body for Empty { - fn len(&self) -> Option { - Some(0) - } -} - -/// An incoming HTTP body -#[derive(Debug)] -pub struct IncomingBody { - kind: BodyKind, - // IMPORTANT: the order of these fields here matters. `body_stream` must - // be dropped before `incoming_body`. - body_stream: AsyncInputStream, - incoming_body: WasiIncomingBody, -} - -impl IncomingBody { - pub(crate) fn new( - kind: BodyKind, - body_stream: AsyncInputStream, - incoming_body: WasiIncomingBody, - ) -> Self { - Self { - kind, - body_stream, - incoming_body, - } - } - - /// Consume this `IncomingBody` and return the trailers, if present. - pub async fn finish(self) -> Result, Error> { - // The stream is a child resource of the `IncomingBody`, so ensure that - // it's dropped first. - drop(self.body_stream); - - let trailers = WasiIncomingBody::finish(self.incoming_body); - - AsyncPollable::new(trailers.subscribe()).wait_for().await; - - let trailers = trailers.get().unwrap().unwrap()?; - - let trailers = match trailers { - None => None, - Some(trailers) => Some(header_map_from_wasi(trailers)?), - }; - - Ok(trailers) - } - - /// Try to deserialize the incoming body as JSON. The optional - /// `json` feature is required. - /// - /// Fails whenever the response body is not in JSON format, - /// or it cannot be properly deserialized to target type `T`. For more - /// details please see [`serde_json::from_reader`]. - /// - /// [`serde_json::from_reader`]: https://docs.serde.rs/serde_json/fn.from_reader.html - #[cfg(feature = "json")] - pub async fn json(&mut self) -> Result { - let buf = self.bytes().await?; - serde_json::from_slice(&buf).map_err(|e| ErrorVariant::Other(e.to_string()).into()) - } - - /// Get the full response body as `Vec`. - pub async fn bytes(&mut self) -> Result, Error> { - let mut buf = match self.kind { - BodyKind::Fixed(l) => { - if l > (usize::MAX as u64) { - return Err(ErrorVariant::Other( - "incoming body is too large to allocate and buffer in memory".to_string(), - ) - .into()); - } else { - Vec::with_capacity(l as usize) - } - } - BodyKind::Chunked => Vec::with_capacity(4096), - }; - self.read_to_end(&mut buf).await?; - Ok(buf) - } -} - -#[async_trait::async_trait(?Send)] -impl AsyncRead for IncomingBody { - async fn read(&mut self, out_buf: &mut [u8]) -> crate::io::Result { - self.body_stream.read(out_buf).await - } - - fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { - Some(&self.body_stream) - } -} - -impl Body for IncomingBody { - fn len(&self) -> Option { - match self.kind { - BodyKind::Fixed(l) => { - if l > (usize::MAX as u64) { - None - } else { - Some(l as usize) - } - } - BodyKind::Chunked => None, - } - } -} - -#[derive(Debug)] -pub struct InvalidContentLength; - -impl fmt::Display for InvalidContentLength { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - "incoming content-length should be a u64; violates HTTP/1.1".fmt(f) - } -} - -impl std::error::Error for InvalidContentLength {} - -impl From for Error { - fn from(e: InvalidContentLength) -> Self { - // TODO: What's the right error code here? - ErrorVariant::Other(e.to_string()).into() - } -} - -/// The output stream for the body, implementing [`AsyncWrite`]. Call -/// [`Responder::start_response`] or [`Client::start_request`] to obtain -/// one. Once the body is complete, it must be declared finished, using -/// [`Finished::finish`], [`Finished::fail`], [`Client::finish`], or -/// [`Client::fail`]. -/// -/// [`Responder::start_response`]: crate::http::server::Responder::start_response -/// [`Client::start_request`]: crate::http::client::Client::start_request -/// [`Finished::finish`]: crate::http::server::Finished::finish -/// [`Finished::fail`]: crate::http::server::Finished::fail -/// [`Client::finish`]: crate::http::client::Client::finish -/// [`Client::fail`]: crate::http::client::Client::fail -#[must_use] -pub struct OutgoingBody { - // IMPORTANT: the order of these fields here matters. `stream` must - // be dropped before `body`. - stream: AsyncOutputStream, - body: wasi::http::types::OutgoingBody, - dontdrop: DontDropOutgoingBody, -} - -impl OutgoingBody { - pub(crate) fn new(stream: AsyncOutputStream, body: wasi::http::types::OutgoingBody) -> Self { - Self { - stream, - body, - dontdrop: DontDropOutgoingBody, - } - } - - pub(crate) fn consume(self) -> (AsyncOutputStream, wasi::http::types::OutgoingBody) { - let Self { - stream, - body, - dontdrop, - } = self; - - std::mem::forget(dontdrop); - - (stream, body) - } - - /// Return a reference to the underlying `AsyncOutputStream`. - /// - /// This usually isn't needed, as `OutgoingBody` implements `AsyncWrite` - /// too, however it is useful for code that expects to work with - /// `AsyncOutputStream` specifically. - pub fn stream(&mut self) -> &mut AsyncOutputStream { - &mut self.stream - } -} - -#[async_trait::async_trait(?Send)] -impl AsyncWrite for OutgoingBody { - async fn write(&mut self, buf: &[u8]) -> crate::io::Result { - self.stream.write(buf).await - } - - async fn flush(&mut self) -> crate::io::Result<()> { - self.stream.flush().await - } - - fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { - Some(&self.stream) - } -} - -/// A utility to ensure that `OutgoingBody` is either finished or failed, and -/// not implicitly dropped. -struct DontDropOutgoingBody; - -impl Drop for DontDropOutgoingBody { - fn drop(&mut self) { - unreachable!("`OutgoingBody::drop` called; `OutgoingBody`s should be consumed with `finish` or `fail`."); - } -} - -/// A placeholder for use as the type parameter to [`Request`] and [`Response`] -/// to indicate that the body has not yet started. This is used with -/// [`Client::start_request`] and [`Responder::start_response`], which have -/// `Requeset` and `Response` arguments, -/// respectively. -/// -/// To instead start the response and obtain the output stream for the body, -/// use [`Responder::respond`]. -/// To instead send a request or response with an input stream for the body, -/// use [`Client::send`] or [`Responder::respond`]. -/// -/// [`Request`]: crate::http::Request -/// [`Response`]: crate::http::Response -/// [`Client::start_request`]: crate::http::Client::start_request -/// [`Responder::start_response`]: crate::http::server::Responder::start_response -/// [`Client::send`]: crate::http::Client::send -/// [`Responder::respond`]: crate::http::server::Responder::respond -pub struct BodyForthcoming; diff --git a/src/http/client.rs b/src/http/client.rs index 582cddb..49613b9 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -6,7 +6,6 @@ use crate::time::Duration; use wasip2::http::types::RequestOptions as WasiRequestOptions; /// An HTTP client. -// Empty for now, but permits adding support for RequestOptions soon: #[derive(Debug)] pub struct Client { options: Option, @@ -25,11 +24,7 @@ impl Client { } /// Send an HTTP request. - pub async fn send>( - &self, - req: Request, - // FIXME change this anyhow::Error to crate::http::Error - ) -> Result, anyhow::Error> { + pub async fn send>(&self, req: Request) -> Result, Error> { let (wasi_req, body) = try_into_outgoing(req)?; let body = body.into(); let wasi_body = wasi_req.body().unwrap(); @@ -50,7 +45,7 @@ impl Client { // is to trap if we try and get the response more than once. The final // `?` is to raise the actual error if there is one. let res = res.get().unwrap().unwrap()?; - Ok(try_from_incoming(res)?) + try_from_incoming(res) }, ) .await?; @@ -76,7 +71,7 @@ impl Client { match &mut self.options { Some(o) => o, uninit => { - *uninit = Some(Default::default()); + *uninit = Some(RequestOptions::default()); uninit.as_mut().unwrap() } } @@ -102,18 +97,22 @@ impl RequestOptions { let wasi = WasiRequestOptions::new(); if let Some(timeout) = self.connect_timeout { wasi.set_connect_timeout(Some(timeout.0)).map_err(|()| { - Error::other("wasi-http implementation does not support connect timeout option") + anyhow::Error::msg( + "wasi-http implementation does not support connect timeout option", + ) })?; } if let Some(timeout) = self.first_byte_timeout { wasi.set_first_byte_timeout(Some(timeout.0)).map_err(|()| { - Error::other("wasi-http implementation does not support first byte timeout option") + anyhow::Error::msg( + "wasi-http implementation does not support first byte timeout option", + ) })?; } if let Some(timeout) = self.between_bytes_timeout { wasi.set_between_bytes_timeout(Some(timeout.0)) .map_err(|()| { - Error::other( + anyhow::Error::msg( "wasi-http implementation does not support between byte timeout option", ) })?; diff --git a/src/http/error.rs b/src/http/error.rs index 8140c08..a4f22b0 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -1,130 +1,13 @@ -use crate::http::fields::ToWasiHeaderError; -use std::fmt; - -/// The `http` result type. -pub type Result = std::result::Result; - -/// The `http` error type. -pub struct Error { - variant: ErrorVariant, - context: Vec, -} +//! The http portion of wstd uses `anyhow::Error` as its `Error` type. +//! +//! There are various concrete error types +pub use crate::http::body::InvalidContentLength; +pub use anyhow::Context; pub use http::header::{InvalidHeaderName, InvalidHeaderValue}; pub use http::method::InvalidMethod; pub use wasip2::http::types::{ErrorCode, HeaderError}; -impl fmt::Debug for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for c in self.context.iter() { - writeln!(f, "in {c}:")?; - } - match &self.variant { - ErrorVariant::WasiHttp(e) => write!(f, "wasi http error: {e:?}"), - ErrorVariant::WasiHeader(e) => write!(f, "wasi header error: {e:?}"), - ErrorVariant::HeaderName(e) => write!(f, "header name error: {e:?}"), - ErrorVariant::HeaderValue(e) => write!(f, "header value error: {e:?}"), - ErrorVariant::Method(e) => write!(f, "method error: {e:?}"), - ErrorVariant::BodyIo(e) => write!(f, "body error: {e:?}"), - ErrorVariant::Other(e) => write!(f, "{e}"), - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.variant { - ErrorVariant::WasiHttp(e) => write!(f, "wasi http error: {e}"), - ErrorVariant::WasiHeader(e) => write!(f, "wasi header error: {e}"), - ErrorVariant::HeaderName(e) => write!(f, "header name error: {e}"), - ErrorVariant::HeaderValue(e) => write!(f, "header value error: {e}"), - ErrorVariant::Method(e) => write!(f, "method error: {e}"), - ErrorVariant::BodyIo(e) => write!(f, "body error: {e}"), - ErrorVariant::Other(e) => write!(f, "{e}"), - } - } -} - -impl std::error::Error for Error {} - -impl Error { - pub fn variant(&self) -> &ErrorVariant { - &self.variant - } - pub(crate) fn other(s: impl Into) -> Self { - ErrorVariant::Other(s.into()).into() - } - pub(crate) fn context(self, s: impl Into) -> Self { - let mut context = self.context; - context.push(s.into()); - Self { - variant: self.variant, - context, - } - } -} - -impl From for Error { - fn from(variant: ErrorVariant) -> Error { - Error { - variant, - context: Vec::new(), - } - } -} - -impl From for Error { - fn from(e: ErrorCode) -> Error { - ErrorVariant::WasiHttp(e).into() - } -} - -impl From for Error { - fn from(error: ToWasiHeaderError) -> Error { - Error { - variant: ErrorVariant::WasiHeader(error.error), - context: vec![error.context], - } - } -} - -impl From for Error { - fn from(e: InvalidHeaderValue) -> Error { - ErrorVariant::HeaderValue(e).into() - } -} - -impl From for Error { - fn from(e: InvalidHeaderName) -> Error { - ErrorVariant::HeaderName(e).into() - } -} - -impl From for Error { - fn from(e: InvalidMethod) -> Error { - ErrorVariant::Method(e).into() - } -} - -impl From for Error { - fn from(e: std::io::Error) -> Error { - ErrorVariant::BodyIo(e).into() - } -} - -impl From for Error { - fn from(e: super::body::InvalidContentLength) -> Error { - ErrorVariant::Other(e.to_string()).into() - } -} - -#[derive(Debug)] -pub enum ErrorVariant { - WasiHttp(ErrorCode), - WasiHeader(HeaderError), - HeaderName(InvalidHeaderName), - HeaderValue(InvalidHeaderValue), - Method(InvalidMethod), - BodyIo(std::io::Error), - Other(String), -} +pub type Error = anyhow::Error; +/// The `http` result type. +pub type Result = std::result::Result; diff --git a/src/http/fields.rs b/src/http/fields.rs index f476cc9..35a9be7 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -1,43 +1,27 @@ pub use http::header::{HeaderMap, HeaderName, HeaderValue}; -use super::Error; -use core::fmt; -use wasip2::http::types::{Fields, HeaderError as WasiHttpHeaderError}; +use super::{error::Context, Error}; +use wasip2::http::types::Fields; pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { let mut output = HeaderMap::new(); for (key, value) in wasi_fields.entries() { - let key = HeaderName::from_bytes(key.as_bytes()) - .map_err(|e| Error::from(e).context("header name {key}"))?; - let value = HeaderValue::from_bytes(&value) - .map_err(|e| Error::from(e).context("header value for {key}"))?; + let key = + HeaderName::from_bytes(key.as_bytes()).with_context(|| format!("header name {key}"))?; + let value = + HeaderValue::from_bytes(&value).with_context(|| format!("header value for {key}"))?; output.append(key, value); } Ok(output) } -pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Result { +pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Result { let wasi_fields = Fields::new(); for (key, value) in header_map { // Unwrap because `HeaderMap` has already validated the headers. wasi_fields .append(key.as_str(), value.as_bytes()) - .map_err(|error| ToWasiHeaderError { - error, - context: format!("header {key}: {value:?}"), - })?; + .with_context(|| format!("wasi rejected header `{key}: {value:?}`"))? } Ok(wasi_fields) } - -#[derive(Debug)] -pub(crate) struct ToWasiHeaderError { - pub(crate) error: WasiHttpHeaderError, - pub(crate) context: String, -} -impl fmt::Display for ToWasiHeaderError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - core::write!(f, "{}: {:?}", self.context, self.error) - } -} -impl core::error::Error for ToWasiHeaderError {} diff --git a/src/http/mod.rs b/src/http/mod.rs index e53e038..fd607d8 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -6,7 +6,7 @@ pub use http::uri::{Authority, PathAndQuery, Uri}; #[doc(inline)] pub use body::{util::BodyExt, Body, Incoming}; pub use client::Client; -pub use error::{Error, Result}; +pub use error::{Error, ErrorCode, Result}; pub use fields::{HeaderMap, HeaderName, HeaderValue}; pub use method::Method; pub use request::Request; diff --git a/src/http/request.rs b/src/http/request.rs index 17e3252..aeec352 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,10 +1,10 @@ use super::{ body::{BodyHint, Incoming}, - error::ErrorCode, + error::{Context, Error, ErrorCode}, fields::{header_map_from_wasi, header_map_to_wasi}, method::{from_wasi_method, to_wasi_method}, scheme::{from_wasi_scheme, to_wasi_scheme}, - Authority, Error, HeaderMap, PathAndQuery, Uri, + Authority, HeaderMap, PathAndQuery, Uri, }; use wasip2::http::outgoing_handler::OutgoingRequest; use wasip2::http::types::IncomingRequest; @@ -22,7 +22,7 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque let method = to_wasi_method(parts.method); wasi_req .set_method(&method) - .map_err(|()| Error::other(format!("method rejected by wasi-http: {method:?}",)))?; + .map_err(|()| anyhow::anyhow!("method rejected by wasi-http: {method:?}"))?; // Set the url scheme let scheme = parts @@ -32,21 +32,19 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque .unwrap_or(wasip2::http::types::Scheme::Https); wasi_req .set_scheme(Some(&scheme)) - .map_err(|()| Error::other(format!("scheme rejected by wasi-http: {scheme:?}")))?; + .map_err(|()| anyhow::anyhow!("scheme rejected by wasi-http: {scheme:?}"))?; // Set authority let authority = parts.uri.authority().map(Authority::as_str); wasi_req .set_authority(authority) - .map_err(|()| Error::other(format!("authority rejected by wasi-http {authority:?}")))?; + .map_err(|()| anyhow::anyhow!("authority rejected by wasi-http {authority:?}"))?; // Set the url path + query string if let Some(p_and_q) = parts.uri.path_and_query() { wasi_req .set_path_with_query(Some(p_and_q.as_str())) - .map_err(|()| { - Error::other(format!("path and query rejected by wasi-http {p_and_q:?}")) - })?; + .map_err(|()| anyhow::anyhow!("path and query rejected by wasi-http {p_and_q:?}"))?; } // All done; request is ready for send-off @@ -55,33 +53,40 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque /// This is used by the `http_server` macro. #[doc(hidden)] -pub fn try_from_incoming(incoming: IncomingRequest) -> Result, ErrorCode> { - // TODO: What's the right error code to use for invalid headers? +pub fn try_from_incoming(incoming: IncomingRequest) -> Result, Error> { let headers: HeaderMap = header_map_from_wasi(incoming.headers()) - .map_err(|e| ErrorCode::InternalError(Some(e.to_string())))?; + .context("headers provided by wasi rejected by http::HeaderMap")?; let method = from_wasi_method(incoming.method()).map_err(|_| ErrorCode::HttpRequestMethodInvalid)?; - let scheme = incoming.scheme().map(|scheme| { - from_wasi_scheme(scheme).expect("TODO: what shall we do with an invalid uri here?") - }); - let authority = incoming.authority().map(|authority| { - Authority::from_maybe_shared(authority) - .expect("TODO: what shall we do with an invalid uri authority here?") - }); - let path_and_query = incoming.path_with_query().map(|path_and_query| { - PathAndQuery::from_maybe_shared(path_and_query) - .expect("TODO: what shall we do with an invalid uri path-and-query here?") - }); + let scheme = incoming + .scheme() + .map(|scheme| { + from_wasi_scheme(scheme).context("scheme provided by wasi rejected by http::Scheme") + }) + .transpose()?; + let authority = incoming + .authority() + .map(|authority| { + Authority::from_maybe_shared(authority) + .context("authority provided by wasi rejected by http::Authority") + }) + .transpose()?; + let path_and_query = incoming + .path_with_query() + .map(|path_and_query| { + PathAndQuery::from_maybe_shared(path_and_query) + .context("path and query provided by wasi rejected by http::PathAndQuery") + }) + .transpose()?; + + let hint = BodyHint::from_headers(&headers)?; - // TODO: What's the right error code to use for invalid headers? - let hint = BodyHint::from_headers(&headers) - .map_err(|e| ErrorCode::InternalError(Some(e.to_string())))?; // `body_stream` is a child of `incoming_body` which means we cannot // drop the parent before we drop the child let incoming_body = incoming .consume() - .expect("cannot call `consume` twice on incoming request"); + .expect("`consume` should not have been called previously on this incoming-request"); let body = Incoming::new(incoming_body, hint); let mut uri = Uri::builder(); @@ -94,17 +99,11 @@ pub fn try_from_incoming(incoming: IncomingRequest) -> Result, if let Some(path_and_query) = path_and_query { uri = uri.path_and_query(path_and_query); } - // TODO: What's the right error code to use for an invalid uri? - let uri = uri - .build() - .map_err(|e| ErrorCode::InternalError(Some(e.to_string())))?; + let uri = uri.build().context("building uri from wasi")?; let mut request = Request::builder().method(method).uri(uri); if let Some(headers_mut) = request.headers_mut() { *headers_mut = headers; } - // TODO: What's the right error code to use for an invalid request? - request - .body(body) - .map_err(|e| ErrorCode::InternalError(Some(e.to_string()))) + request.body(body).context("building request from wasi") } diff --git a/src/http/response.rs b/src/http/response.rs index f17b868..d7142e3 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -1,19 +1,17 @@ +use http::StatusCode; use wasip2::http::types::IncomingResponse; -use super::{ - body::{BodyHint, Incoming}, - fields::header_map_from_wasi, - Error, HeaderMap, -}; -use http::StatusCode; +use crate::http::body::{BodyHint, Incoming}; +use crate::http::error::{Context, Error}; +use crate::http::fields::{header_map_from_wasi, HeaderMap}; pub use http::response::{Builder, Response}; pub(crate) fn try_from_incoming(incoming: IncomingResponse) -> Result, Error> { let headers: HeaderMap = header_map_from_wasi(incoming.headers())?; // TODO: Does WASI guarantee that the incoming status is valid? - let status = - StatusCode::from_u16(incoming.status()).map_err(|err| Error::other(err.to_string()))?; + let status = StatusCode::from_u16(incoming.status()) + .map_err(|err| anyhow::anyhow!("wasi provided invalid status code ({err})"))?; let hint = BodyHint::from_headers(&headers)?; // `body_stream` is a child of `incoming_body` which means we cannot @@ -29,7 +27,5 @@ pub(crate) fn try_from_incoming(incoming: IncomingResponse) -> Result, responder: Responder) -> Finished { -//! responder -//! .respond(Response::new("Hello!\n".into_body())) -//! .await +//! async fn main(_request: Request) -> Result, Error> { +//! Ok(Response::new("Hello!\n".to_string().into())) //! } //! ``` //! @@ -20,18 +18,12 @@ //! [`Response`]: crate::http::Response //! [`http_server`]: crate::http_server -use super::{error::ErrorCode, fields::header_map_to_wasi, Body, Response}; -use anyhow::Error; +use super::{error::ErrorCode, fields::header_map_to_wasi, Body, Error, Response}; use http::header::CONTENT_LENGTH; use wasip2::exports::http::incoming_handler::ResponseOutparam; use wasip2::http::types::OutgoingResponse; -/// TK REWRITE THESE DOCS -/// This is passed into the [`http_server`] `main` function and holds the state -/// needed for a handler to produce a response, or fail. There are two ways to -/// respond, with [`Responder::start_response`] to stream the body in, or -/// [`Responder::respond`] to give the body as a string, byte array, or input -/// stream. See those functions for examples. +/// For use by the [`http_server`] macro only. /// /// [`http_server`]: crate::http_server #[must_use] @@ -40,24 +32,8 @@ pub struct Responder { } impl Responder { - /// Respond with the given `Response` which contains the body. - /// - /// If the body has a known length, a Content-Length header is automatically added. - /// - /// # Example - /// - /// ``` - /// # use wstd::http::{Response, Request}; - /// # use wstd::http::body::BodyExt; - /// # use wstd::http::server::{Finished, Responder}; - /// # - /// # async fn example(responder: Responder) { - /// responder - /// .respond(Response::new("Hello!\n".into_body())) - /// .await - /// # } - /// # fn main() {} - /// ``` + /// This is used by the `http_server` macro. + #[doc(hidden)] pub async fn respond>(self, response: Response) -> Result<(), Error> { let headers = response.headers(); let status = response.status().as_u16(); @@ -83,9 +59,12 @@ impl Responder { // Unwrap because we can be sure we only call these once. let wasi_body = wasi_response.body().unwrap(); - // Tell WASI to start the show. + // Set the outparam to the response, which allows wasi-http to send + // the response status and headers. ResponseOutparam::set(self.outparam, Ok(wasi_response)); + // Then send the body. The response will be fully sent once this + // future is ready. body.send(wasi_body).await } @@ -97,8 +76,12 @@ impl Responder { /// This is used by the `http_server` macro. #[doc(hidden)] - pub fn fail(self, err: ErrorCode) -> Result<(), Error> { - ResponseOutparam::set(self.outparam, Err(err.clone())); - Err(err.into()) + pub fn fail(self, err: Error) -> Result<(), Error> { + let e = match err.downcast_ref::() { + Some(e) => e.clone(), + None => ErrorCode::InternalError(Some(format!("{err:?}"))), + }; + ResponseOutparam::set(self.outparam, Err(e)); + Err(err) } } diff --git a/tests/http_first_byte_timeout.rs b/tests/http_first_byte_timeout.rs index ec15248..4882966 100644 --- a/tests/http_first_byte_timeout.rs +++ b/tests/http_first_byte_timeout.rs @@ -1,7 +1,4 @@ -use wstd::http::{ - error::{Error, ErrorCode, ErrorVariant}, - Body, Client, Request, -}; +use wstd::http::{error::ErrorCode, Body, Client, Request}; #[wstd::main] async fn main() -> Result<(), Box> { @@ -17,8 +14,8 @@ async fn main() -> Result<(), Box> { let error = result.unwrap_err(); assert!( matches!( - error.downcast_ref::().map(|e| e.variant()), - Some(ErrorVariant::WasiHttp(ErrorCode::ConnectionReadTimeout)) + error.downcast_ref::(), + Some(ErrorCode::ConnectionReadTimeout) ), "expected ConnectionReadTimeout error, got: {error:?>}" );