From 083dc21893292c5d9767f4c8a4164490c010d19e Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Thu, 2 Jan 2025 10:28:20 -0800 Subject: [PATCH 01/19] Implement an HTTP server framework. Add a wstd API for creating HTTP server applications, wrapping the WASI proxy world trait and macro. Compiling the example with `RUSTFLAGS="-Clink-arg=--wasi-adapter=proxy"` produces a program that runs in `wasmtime serve`. --- Cargo.toml | 2 + examples/http_server.rs | 69 ++++++++++++++ macro/src/lib.rs | 72 ++++++++++++++ src/http/body.rs | 78 ++++++++++++++- src/http/client.rs | 2 + src/http/fields.rs | 49 +++++++++- src/http/method.rs | 6 +- src/http/mod.rs | 7 +- src/http/request.rs | 99 +++++++++++++++++-- src/http/response.rs | 6 +- src/http/scheme.rs | 20 ++++ src/http/server.rs | 204 ++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 12 +++ 13 files changed, 605 insertions(+), 21 deletions(-) create mode 100644 examples/http_server.rs create mode 100644 src/http/scheme.rs create mode 100644 src/http/server.rs diff --git a/Cargo.toml b/Cargo.toml index d360479..e546d04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ categories.workspace = true [dependencies] futures-core.workspace = true http.workspace = true +itoa.workspace = true pin-project-lite.workspace = true slab.workspace = true wasi.workspace = true @@ -52,6 +53,7 @@ futures-core = "0.3.19" futures-lite = "1.12.0" heck = "0.5" http = "1.1" +itoa = "1" pin-project-lite = "0.2.8" quote = "1.0" serde_json = "1" diff --git a/examples/http_server.rs b/examples/http_server.rs new file mode 100644 index 0000000..96b53f1 --- /dev/null +++ b/examples/http_server.rs @@ -0,0 +1,69 @@ +use wstd::http::body::{BodyForthcoming, IncomingBody, OutgoingBody}; +use wstd::http::server::{Finished, Responder}; +use wstd::http::{IntoBody, Request, Response}; +use wstd::io::{copy, AsyncWrite}; + +#[wstd::http_server] +async fn main(request: Request, responder: Responder) -> Finished { + match request.uri().path_and_query().unwrap().as_str() { + "/wait" => http_wait(request, responder).await, + "/echo" => http_echo(request, responder).await, + "/fail" => http_fail(request, responder).await, + "/bigfail" => http_bigfail(request, responder).await, + "/" | _ => http_home(request, responder).await, + } +} + +async fn http_home(_request: Request, responder: Responder) -> Finished { + // 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 +} + +async fn http_wait(_request: Request, responder: Responder) -> Finished { + // Get the time now + let now = wasi::clocks::monotonic_clock::now(); + + // Sleep for 1 second + let nanos = 1_000_000_000; + let pollable = wasi::clocks::monotonic_clock::subscribe_duration(nanos); + pollable.block(); + + // Compute how long we slept for. + let elapsed = wasi::clocks::monotonic_clock::now() - now; + let elapsed = elapsed / 1_000_000; // change to 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) +} + +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_fail(_request: Request, responder: Responder) -> Finished { + let body = responder.start_response(Response::new(BodyForthcoming)); + Finished::fail(body) +} + +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(()) + } + + let mut body = responder.start_response(Response::new(BodyForthcoming)); + let _ = write_body(&mut body).await; + Finished::fail(body) +} diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 2f8712c..9c19e06 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -83,3 +83,75 @@ pub fn attr_macro_test(_attr: TokenStream, item: TokenStream) -> TokenStream { } .into() } + +/// Enables a HTTP-server main function, for creating [HTTP servers]. +/// +/// [HTTP servers]: https://docs.rs/wstd/latest/wstd/http/server/index.html +/// +/// # Examples +/// +/// ```ignore +/// #[wstd::http_server] +/// async fn main(request: Request, responder: Responder) -> Finished { +/// responder +/// .respond(Response::new("Hello!\n".into_body())) +/// .await +/// } +/// ``` +#[proc_macro_attribute] +pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + + if input.sig.asyncness.is_none() { + return quote_spanned! { input.sig.fn_token.span()=> + compile_error!("fn must be `async fn`"); + } + .into(); + } + + let output = &input.sig.output; + let inputs = &input.sig.inputs; + let name = &input.sig.ident; + let body = &input.block; + let attrs = &input.attrs; + let vis = &input.vis; + + if name != "main" { + return quote_spanned! { input.sig.ident.span()=> + compile_error!("only `async fn main` can be used for #[wstd::http_server]"); + } + .into(); + } + + quote! { + struct TheServer; + + impl ::wstd::wasi::exports::http::incoming_handler::Guest for TheServer { + fn handle( + request: ::wstd::wasi::http::types::IncomingRequest, + response_out: ::wstd::wasi::http::types::ResponseOutparam + ) { + #(#attrs)* + #vis async fn __run(#inputs) #output { + #body + } + + let responder = ::wstd::http::server::Responder::new(response_out); + let _finished: ::wstd::http::server::Finished = + match ::wstd::http::try_from_incoming_request(request) + { + Ok(request) => ::wstd::runtime::block_on(async { __run(request, responder).await }), + Err(err) => responder.fail(err), + }; + } + } + + ::wstd::wasi::http::proxy::export!(TheServer with_types_in ::wstd::wasi); + + // In case the user needs it, provide a `main` function so that the + // code compiles. + #[allow(dead_code)] + fn main() { unreachable!("HTTP-server components should be run wth `handle` rather than `main`") } + } + .into() +} diff --git a/src/http/body.rs b/src/http/body.rs index 7352cb4..38bb419 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -1,6 +1,6 @@ //! HTTP body types -use crate::io::{AsyncInputStream, AsyncRead, Cursor, Empty}; +use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Cursor, Empty}; use core::fmt; use http::header::{CONTENT_LENGTH, TRANSFER_ENCODING}; use wasi::http::types::IncomingBody as WasiIncomingBody; @@ -177,3 +177,79 @@ impl From for Error { ErrorVariant::Other(e.to_string()).into() } } + +/// The output stream for the body, implementing [`AsyncWrite`]. Call +/// [`Responder::start_response`] to obtain one. Once the body is complete, +/// it must be declared finished, using [`OutgoingBody::finish`]. +#[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 + } +} + +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 [`Response`] to indicate +/// that the body has not yet started. This is used with +/// [`Responder::start_response`], which has a `Response` +/// argument. +/// +/// To instead start the response and obtain the output stream for the body, +/// use [`Responder::respond`]. +pub struct BodyForthcoming; diff --git a/src/http/client.rs b/src/http/client.rs index ae6e948..f6d576e 100644 --- a/src/http/client.rs +++ b/src/http/client.rs @@ -20,6 +20,8 @@ impl Client { /// Send an HTTP request. 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 body_stream = wasi_body.write().unwrap(); diff --git a/src/http/fields.rs b/src/http/fields.rs index 22f7093..9b4c86b 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -1,6 +1,9 @@ pub use http::header::{HeaderMap, HeaderName, HeaderValue}; +use http::header::{InvalidHeaderName, InvalidHeaderValue}; +use super::error::ErrorVariant; use super::{Error, Result}; +use std::fmt; use wasi::http::types::Fields; pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { @@ -15,12 +18,52 @@ pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { Ok(output) } -pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Result { +pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Fields { let wasi_fields = Fields::new(); for (key, value) in header_map { + // Unwrap because `HeaderMap` has already validated the headers. + // TODO: Remove the `to_owned()` calls after bytecodealliance/wit-bindgen#1102. wasi_fields .append(&key.as_str().to_owned(), &value.as_bytes().to_owned()) - .map_err(|e| Error::from(e).context("header named {key}"))?; + .unwrap_or_else(|err| panic!("header named {key}: {err:?}")); + } + wasi_fields +} + +#[derive(Debug)] +pub(crate) enum InvalidHeader { + Name(InvalidHeaderName), + Value(InvalidHeaderValue), +} + +impl fmt::Display for InvalidHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Name(e) => e.fmt(f), + Self::Value(e) => e.fmt(f), + } + } +} + +impl std::error::Error for InvalidHeader {} + +impl From for InvalidHeader { + fn from(e: InvalidHeaderName) -> Self { + Self::Name(e) + } +} + +impl From for InvalidHeader { + fn from(e: InvalidHeaderValue) -> Self { + Self::Value(e) + } +} + +impl From for Error { + fn from(e: InvalidHeader) -> Self { + match e { + InvalidHeader::Name(e) => ErrorVariant::HeaderName(e).into(), + InvalidHeader::Value(e) => ErrorVariant::HeaderValue(e).into(), + } } - Ok(wasi_fields) } diff --git a/src/http/method.rs b/src/http/method.rs index bd7c210..1f06eff 100644 --- a/src/http/method.rs +++ b/src/http/method.rs @@ -1,6 +1,6 @@ use wasi::http::types::Method as WasiMethod; -use super::Result; +use http::method::InvalidMethod; pub use http::Method; pub(crate) fn to_wasi_method(value: Method) -> WasiMethod { @@ -18,9 +18,7 @@ pub(crate) fn to_wasi_method(value: Method) -> WasiMethod { } } -// This will become useful once we support IncomingRequest -#[allow(dead_code)] -pub(crate) fn from_wasi_method(value: WasiMethod) -> Result { +pub(crate) fn from_wasi_method(value: WasiMethod) -> Result { Ok(match value { WasiMethod::Get => Method::GET, WasiMethod::Head => Method::HEAD, diff --git a/src/http/mod.rs b/src/http/mod.rs index 1bc1aa3..2c73d93 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,7 +1,7 @@ //! HTTP networking support //! pub use http::status::StatusCode; -pub use http::uri::Uri; +pub use http::uri::{Authority, PathAndQuery, Uri}; #[doc(inline)] pub use body::{Body, IntoBody}; @@ -9,8 +9,9 @@ pub use client::Client; pub use error::{Error, Result}; pub use fields::{HeaderMap, HeaderName, HeaderValue}; pub use method::Method; -pub use request::Request; +pub use request::{try_from_incoming_request, Request}; pub use response::Response; +pub use scheme::{InvalidUri, Scheme}; pub mod body; @@ -20,3 +21,5 @@ mod fields; mod method; mod request; mod response; +mod scheme; +pub mod server; diff --git a/src/http/request.rs b/src/http/request.rs index 21411ad..1b40047 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -1,11 +1,19 @@ -use super::{fields::header_map_to_wasi, method::to_wasi_method, Error, Result}; +use super::{ + body::{BodyKind, IncomingBody}, + error::WasiHttpErrorCode, + 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 wasi::http::outgoing_handler::OutgoingRequest; -use wasi::http::types::Scheme; +use wasi::http::types::IncomingRequest; pub use http::Request; -pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingRequest, T)> { - let wasi_req = OutgoingRequest::new(header_map_to_wasi(request.headers())?); +pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingRequest, T), Error> { + let wasi_req = OutgoingRequest::new(header_map_to_wasi(request.headers())); let (parts, body) = request.into_parts(); @@ -16,11 +24,11 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque .map_err(|()| Error::other(format!("method rejected by wasi-http: {method:?}",)))?; // Set the url scheme - let scheme = match parts.uri.scheme().map(|s| s.as_str()) { - Some("http") => Scheme::Http, - Some("https") | None => Scheme::Https, - Some(other) => Scheme::Other(other.to_owned()), - }; + let scheme = parts + .uri + .scheme() + .map(to_wasi_scheme) + .unwrap_or(wasi::http::types::Scheme::Https); wasi_req .set_scheme(Some(&scheme)) .map_err(|()| Error::other(format!("scheme rejected by wasi-http: {scheme:?}")))?; @@ -33,6 +41,7 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque // Set the url path + query string if let Some(p_and_q) = parts.uri.path_and_query() { + // TODO: Change the `to_string()` to `as_str()` after bytecodealliance/wit-bindgen#1102. wasi_req .set_path_with_query(Some(&p_and_q.to_string())) .map_err(|()| { @@ -43,3 +52,75 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque // All done; request is ready for send-off Ok((wasi_req, body)) } + +/// This is used by the `http_server` macro. +#[doc(hidden)] +pub fn try_from_incoming_request( + incoming: IncomingRequest, +) -> Result, WasiHttpErrorCode> { + // 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())))?; + + let method = from_wasi_method(incoming.method()) + .map_err(|_| WasiHttpErrorCode::HttpRequestMethodInvalid)?; + let scheme = match incoming.scheme() { + Some(scheme) => Some( + from_wasi_scheme(scheme).expect("TODO: what shall we do with an invalid uri here?"), + ), + None => None, + }; + let authority = match incoming.authority() { + Some(authority) => Some( + Authority::from_maybe_shared(authority) + .expect("TODO: what shall we do with an invalid uri authority here?"), + ), + None => None, + }; + let path_and_query = match incoming.path_with_query() { + Some(path_and_query) => Some( + PathAndQuery::from_maybe_shared(path_and_query) + .expect("TODO: what shall we do with an invalid uri path-and-query here?"), + ), + None => None, + }; + + // 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())))?; + // `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"); + let body_stream = incoming_body + .stream() + .expect("cannot call `stream` twice on an incoming body"); + let body_stream = AsyncInputStream::new(body_stream); + + let body = IncomingBody::new(kind, body_stream, incoming_body); + + let mut uri = Uri::builder(); + if let Some(scheme) = scheme { + uri = uri.scheme(scheme); + } + if let Some(authority) = authority { + uri = uri.authority(authority); + } + 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| WasiHttpErrorCode::InternalError(Some(e.to_string())))?; + + 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| WasiHttpErrorCode::InternalError(Some(e.to_string()))) +} diff --git a/src/http/response.rs b/src/http/response.rs index a2b243a..ed7577d 100644 --- a/src/http/response.rs +++ b/src/http/response.rs @@ -3,14 +3,16 @@ use wasi::http::types::IncomingResponse; use super::{ body::{BodyKind, IncomingBody}, fields::header_map_from_wasi, - Error, HeaderMap, Result, + Error, HeaderMap, }; use crate::io::AsyncInputStream; use http::StatusCode; pub use http::Response; -pub(crate) fn try_from_incoming(incoming: IncomingResponse) -> Result> { +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 = diff --git a/src/http/scheme.rs b/src/http/scheme.rs new file mode 100644 index 0000000..860ce35 --- /dev/null +++ b/src/http/scheme.rs @@ -0,0 +1,20 @@ +use wasi::http::types::Scheme as WasiScheme; + +pub use http::uri::{InvalidUri, Scheme}; +use std::str::FromStr; + +pub(crate) fn to_wasi_scheme(value: &Scheme) -> WasiScheme { + match value.as_str() { + "http" => WasiScheme::Http, + "https" => WasiScheme::Https, + other => WasiScheme::Other(other.to_owned()), + } +} + +pub(crate) fn from_wasi_scheme(value: WasiScheme) -> Result { + Ok(match value { + WasiScheme::Http => Scheme::HTTP, + WasiScheme::Https => Scheme::HTTPS, + WasiScheme::Other(other) => Scheme::from_str(&other)?, + }) +} diff --git a/src/http/server.rs b/src/http/server.rs new file mode 100644 index 0000000..62c01fc --- /dev/null +++ b/src/http/server.rs @@ -0,0 +1,204 @@ +//! HTTP servers +//! +//! The WASI HTTP server API uses the [typed main] idiom, with a `main` function +//! that takes a [`Request`] and a [`Responder`], and responds with a [`Response`], +//! using the [`http_server`] macro: +//! +//! ```no_run +//! #[wstd::http_server] +//! async fn main(request: Request, responder: Responder) -> Finished { +//! responder +//! .respond(Response::new("Hello!\n".into_body())) +//! .await +//! } +//! ``` +//! +//! [typed main]: https://sunfishcode.github.io/typed-main-wasi-presentation/chapter_1.html +//! [`Request`]: crate::http::Request +//! [`Responder`]: crate::http::server::Responder +//! [`Response`]: crate::http::Response +//! [`http_server`]: crate::http_server + +use super::{ + body::{BodyForthcoming, OutgoingBody}, + error::WasiHttpErrorCode, + fields::header_map_to_wasi, + Body, HeaderMap, Response, +}; +use crate::io::{copy, AsyncOutputStream}; +use http::header::CONTENT_LENGTH; +use wasi::exports::http::incoming_handler::ResponseOutparam; +use wasi::http::types::OutgoingResponse; + +/// 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. +/// +/// [`http_server`]: crate::http_server +#[must_use] +pub struct Responder { + outparam: ResponseOutparam, +} + +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) + /// # } + /// ``` + pub fn start_response(self, response: Response) -> OutgoingBody { + let wasi_headers = header_map_to_wasi(response.headers()); + 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::server::{Finished, Responder}; + /// # + /// # async fn example(responder: Responder) -> Finished { + /// responder + /// .respond(Response::new("Hello!\n".into_body())) + /// .await + /// # } + /// ``` + pub async fn respond(self, response: Response) -> Finished { + let headers = response.headers(); + let status = response.status().as_u16(); + + let wasi_headers = header_map_to_wasi(headers); + + // Consume the `response` and prepare to write the body. + let mut body = response.into_body(); + + // Automatically add a Content-Length header. + if let Some(len) = body.len() { + // TODO: Remove the `to_owned()` calls after bytecodealliance/wit-bindgen#1102. + let mut buffer = itoa::Buffer::new(); + wasi_headers + .append( + &CONTENT_LENGTH.as_str().to_owned(), + &buffer.format(len).to_owned().into_bytes(), + ) + .unwrap(); + } + + let wasi_response = OutgoingResponse::new(wasi_headers); + + // Unwrap because `StatusCode` has already validated the status. + 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(); + + // Tell WASI to start the show. + ResponseOutparam::set(self.outparam, Ok(wasi_response)); + + 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) + } + + /// This is used by the `http_server` macro. + #[doc(hidden)] + pub fn new(outparam: ResponseOutparam) -> Self { + Self { outparam } + } + + /// This is used by the `http_server` macro. + #[doc(hidden)] + pub fn fail(self, err: WasiHttpErrorCode) -> Finished { + ResponseOutparam::set(self.outparam, Err(err)); + Finished(()) + } +} + +/// An opaque value returned from a handler indicating that the body is +/// finished, either by [`OutgoingBody::finish`] or [`OutgoingBody::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 result.is_ok() { + let wasi_trailers = trailers.map(|trailers| header_map_to_wasi(&trailers)); + + wasi::http::types::OutgoingBody::finish(body, wasi_trailers) + .expect("body length did not match Content-Length header value"); + } else { + // As in `fail`, there's no need to do anything else on failure. + // TODO: Should we log the failure somewhere? + } + + 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(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index bf96675..fc7d3b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,12 @@ #![doc = include_str!("../tests/http_get.rs")] //! ``` //! +//! **HTTP Server** +//! +//! ```rust,no_run +#![doc = include_str!("../examples/http_server.rs")] +//! ``` +//! //! # Design Decisions //! //! This library is entirely self-contained. This means that it does not share @@ -49,6 +55,7 @@ //! is specific to that are exposed from here. pub mod future; +#[macro_use] pub mod http; pub mod io; pub mod iter; @@ -58,9 +65,14 @@ pub mod runtime; pub mod task; pub mod time; +pub use wstd_macro::attr_macro_http_server as http_server; pub use wstd_macro::attr_macro_main as main; pub use wstd_macro::attr_macro_test as test; +// Re-export the wasi crate for use by the `http_server` macro. +#[doc(hidden)] +pub use wasi; + pub mod prelude { pub use crate::future::FutureExt as _; pub use crate::http::Body as _; From 7f23b8fe08075c413b211954952fd9f37013c21e Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 6 Jan 2025 09:29:48 -0800 Subject: [PATCH 02/19] Add a way to consume trailers from an `IncomingBody`. --- src/http/body.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/http/body.rs b/src/http/body.rs index 38bb419..a301d13 100644 --- a/src/http/body.rs +++ b/src/http/body.rs @@ -1,6 +1,8 @@ //! 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, TRANSFER_ENCODING}; use wasi::http::types::IncomingBody as WasiIncomingBody; @@ -116,9 +118,9 @@ impl Body for Empty { pub struct IncomingBody { kind: BodyKind, // IMPORTANT: the order of these fields here matters. `body_stream` must - // be dropped before `_incoming_body`. + // be dropped before `incoming_body`. body_stream: AsyncInputStream, - _incoming_body: WasiIncomingBody, + incoming_body: WasiIncomingBody, } impl IncomingBody { @@ -130,9 +132,29 @@ impl IncomingBody { Self { kind, body_stream, - _incoming_body: incoming_body, + 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) + } } impl AsyncRead for IncomingBody { From 9441f82bc53ce5d83debabc9986d58485cd8fda2 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 10 Jan 2025 08:16:54 -0800 Subject: [PATCH 03/19] Add an HTTP server test. --- Cargo.toml | 3 + examples/http_server.rs | 20 ++++- test-programs/Cargo.toml | 1 + test-programs/artifacts/Cargo.toml | 3 + test-programs/artifacts/tests/http_server.rs | 89 ++++++++++++++++++++ test-programs/src/bin/http_server.rs | 1 + 6 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 test-programs/artifacts/tests/http_server.rs create mode 100644 test-programs/src/bin/http_server.rs diff --git a/Cargo.toml b/Cargo.toml index e546d04..4dc2d94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ authors = [ [workspace.dependencies] anyhow = "1" cargo_metadata = "0.18.1" +clap = { version = "4.5.23", features = ["derive"] } futures-core = "0.3.19" futures-lite = "1.12.0" heck = "0.5" @@ -62,8 +63,10 @@ syn = "2.0" test-log = { version = "0.2", features = ["trace"] } test-programs = { path = "test-programs" } test-programs-artifacts = { path = "test-programs/artifacts" } +ureq = "2.12.1" wasi = "0.13.1" wasmtime = "26" +wasmtime-cli = "26" wasmtime-wasi = "26" wasmtime-wasi-http = "26" wstd = { path = "." } diff --git a/examples/http_server.rs b/examples/http_server.rs index 96b53f1..1f178d2 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -1,13 +1,15 @@ use wstd::http::body::{BodyForthcoming, IncomingBody, OutgoingBody}; use wstd::http::server::{Finished, Responder}; use wstd::http::{IntoBody, Request, Response}; -use wstd::io::{copy, AsyncWrite}; +use wstd::io::{copy, empty, AsyncWrite}; #[wstd::http_server] async fn main(request: Request, responder: Responder) -> Finished { 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, @@ -67,3 +69,19 @@ async fn http_bigfail(_request: Request, responder: Responder) -> let _ = write_body(&mut body).await; Finished::fail(body) } + +async fn http_echo_headers(request: Request, responder: Responder) -> Finished { + let mut response = Response::builder(); + *response.headers_mut().unwrap() = request.headers().clone(); + let response = response.body(empty()).unwrap(); + responder.respond(response).await +} + +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) +} diff --git a/test-programs/Cargo.toml b/test-programs/Cargo.toml index 633c274..0568427 100644 --- a/test-programs/Cargo.toml +++ b/test-programs/Cargo.toml @@ -8,3 +8,4 @@ publish = false futures-lite.workspace = true serde_json.workspace = true wstd.workspace = true +wasi.workspace = true diff --git a/test-programs/artifacts/Cargo.toml b/test-programs/artifacts/Cargo.toml index 94438a2..152ac54 100644 --- a/test-programs/artifacts/Cargo.toml +++ b/test-programs/artifacts/Cargo.toml @@ -9,9 +9,12 @@ publish = false [dev-dependencies] anyhow.workspace = true +clap.workspace = true test-log.workspace = true test-programs-artifacts.workspace = true +ureq.workspace = true wasmtime.workspace = true +wasmtime-cli.workspace = true wasmtime-wasi.workspace = true wasmtime-wasi-http.workspace = true diff --git a/test-programs/artifacts/tests/http_server.rs b/test-programs/artifacts/tests/http_server.rs new file mode 100644 index 0000000..8303e99 --- /dev/null +++ b/test-programs/artifacts/tests/http_server.rs @@ -0,0 +1,89 @@ +use anyhow::Result; + +fn run_in_wasmtime(wasm: &str) -> Result<()> { + use clap::Parser; + use wasmtime_cli::commands::ServeCommand; + + // Run wasmtime serve. + // Enable -Scli because we build with the default adapter rather than the + // proxy adapter. + // Disable logging so that Wasmtime's tracing_subscriber registration + // doesn't conflict with the test harness' registration. + let serve = + match ServeCommand::try_parse_from(["serve", "-Scli", "-Dlogging=n", wasm].into_iter()) { + Ok(serve) => serve, + Err(e) => { + dbg!(&e); + return Err(e.into()); + } + }; + + serve.execute() +} + +#[test_log::test] +fn http_server() -> Result<()> { + use std::net::TcpStream; + use std::thread::sleep; + use std::time::Duration; + + // Start a `wasmtime serve` server. + let wasmtime_thread = + std::thread::spawn(move || run_in_wasmtime(test_programs_artifacts::HTTP_SERVER)); + + // Clumsily wait for the server to accept connections. + 'wait: loop { + sleep(Duration::from_millis(100)); + if TcpStream::connect("127.0.0.1:8080").is_ok() { + break 'wait; + } + } + + // Do some tests! + + let body: String = ureq::get("http://127.0.0.1:8080").call()?.into_string()?; + assert_eq!(body, "Hello, wasi:http/proxy world!\n"); + + match ureq::get("http://127.0.0.1:8080/fail").call() { + Ok(body) => { + unreachable!("unexpected success from /fail: {:?}", body); + } + Err(ureq::Error::Transport(_transport)) => {} + Err(other) => { + unreachable!("unexpected error: {:?}", other); + } + } + + const MESSAGE: &[u8] = b"hello, echoserver!\n"; + + let body: String = ureq::get("http://127.0.0.1:8080/echo") + .send(MESSAGE)? + .into_string()?; + assert_eq!(body.as_bytes(), MESSAGE); + + let test_headers = [ + ("Red", "Rhubarb"), + ("Orange", "Carrots"), + ("Yellow", "Bananas"), + ("Green", "Broccoli"), + ("Blue", "Blueberries"), + ("Purple", "Beets"), + ]; + + let mut response = ureq::get("http://127.0.0.1:8080/echo-headers"); + for (name, value) in test_headers { + response = response.set(name, value); + } + let response = response.call()?; + + assert!(response.headers_names().len() >= test_headers.len()); + for (name, value) in test_headers { + assert_eq!(response.header(name), Some(value)); + } + + if wasmtime_thread.is_finished() { + wasmtime_thread.join().expect("wasmtime panicked")?; + } + + Ok(()) +} diff --git a/test-programs/src/bin/http_server.rs b/test-programs/src/bin/http_server.rs new file mode 100644 index 0000000..e9fea26 --- /dev/null +++ b/test-programs/src/bin/http_server.rs @@ -0,0 +1 @@ +include!("../../../examples/http_server.rs"); From 77b097e548992216f3f2c0aa409a514f9802c15b Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 10 Jan 2025 10:12:31 -0800 Subject: [PATCH 04/19] Update to wasi 0.14.0, eliminating `.to_owned()` calls. --- Cargo.toml | 2 +- src/http/fields.rs | 3 +-- src/http/request.rs | 3 +-- src/http/server.rs | 6 +----- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4dc2d94..ce27bc2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,7 @@ test-log = { version = "0.2", features = ["trace"] } test-programs = { path = "test-programs" } test-programs-artifacts = { path = "test-programs/artifacts" } ureq = "2.12.1" -wasi = "0.13.1" +wasi = "0.14.0" wasmtime = "26" wasmtime-cli = "26" wasmtime-wasi = "26" diff --git a/src/http/fields.rs b/src/http/fields.rs index 9b4c86b..2727468 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -22,9 +22,8 @@ pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Fields { let wasi_fields = Fields::new(); for (key, value) in header_map { // Unwrap because `HeaderMap` has already validated the headers. - // TODO: Remove the `to_owned()` calls after bytecodealliance/wit-bindgen#1102. wasi_fields - .append(&key.as_str().to_owned(), &value.as_bytes().to_owned()) + .append(&key.as_str(), &value.as_bytes()) .unwrap_or_else(|err| panic!("header named {key}: {err:?}")); } wasi_fields diff --git a/src/http/request.rs b/src/http/request.rs index 1b40047..b34398d 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -41,9 +41,8 @@ pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingReque // Set the url path + query string if let Some(p_and_q) = parts.uri.path_and_query() { - // TODO: Change the `to_string()` to `as_str()` after bytecodealliance/wit-bindgen#1102. wasi_req - .set_path_with_query(Some(&p_and_q.to_string())) + .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:?}")) })?; diff --git a/src/http/server.rs b/src/http/server.rs index 62c01fc..71c3481 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -107,13 +107,9 @@ impl Responder { // Automatically add a Content-Length header. if let Some(len) = body.len() { - // TODO: Remove the `to_owned()` calls after bytecodealliance/wit-bindgen#1102. let mut buffer = itoa::Buffer::new(); wasi_headers - .append( - &CONTENT_LENGTH.as_str().to_owned(), - &buffer.format(len).to_owned().into_bytes(), - ) + .append(&CONTENT_LENGTH.as_str(), &buffer.format(len).as_bytes()) .unwrap(); } From d9bad850e8d1bc1028ad7dad0cde5d9e1ae9d003 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 10 Jan 2025 16:50:26 -0800 Subject: [PATCH 05/19] Use wstd APIs instead of WASI APIs. To support this, add `Duration::as_millis` and related functions. --- examples/http_server.rs | 12 +++++------- src/time/duration.rs | 28 ++++++++++++++++++++++++++++ test-programs/Cargo.toml | 1 - 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/examples/http_server.rs b/examples/http_server.rs index 1f178d2..3ee8b9b 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -2,6 +2,7 @@ use wstd::http::body::{BodyForthcoming, IncomingBody, OutgoingBody}; use wstd::http::server::{Finished, Responder}; use wstd::http::{IntoBody, Request, Response}; use wstd::io::{copy, empty, AsyncWrite}; +use wstd::time::{Duration, Instant}; #[wstd::http_server] async fn main(request: Request, responder: Responder) -> Finished { @@ -25,16 +26,13 @@ async fn http_home(_request: Request, responder: Responder) -> Fin async fn http_wait(_request: Request, responder: Responder) -> Finished { // Get the time now - let now = wasi::clocks::monotonic_clock::now(); + let now = Instant::now(); - // Sleep for 1 second - let nanos = 1_000_000_000; - let pollable = wasi::clocks::monotonic_clock::subscribe_duration(nanos); - pollable.block(); + // Sleep for one second. + wstd::task::sleep(Duration::from_secs(1)).await; // Compute how long we slept for. - let elapsed = wasi::clocks::monotonic_clock::now() - now; - let elapsed = elapsed / 1_000_000; // change to millis + 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)); diff --git a/src/time/duration.rs b/src/time/duration.rs index 10d8103..3571d4c 100644 --- a/src/time/duration.rs +++ b/src/time/duration.rs @@ -70,6 +70,34 @@ impl Duration { pub fn from_secs_f32(secs: f32) -> Duration { std::time::Duration::from_secs_f32(secs).into() } + + /// Returns the number of whole seconds contained by this `Duration`. + #[must_use] + #[inline] + pub const fn as_secs(&self) -> u64 { + self.0 / 1_000_000_000 + } + + /// Returns the number of whole microseconds contained by this `Duration`. + #[must_use] + #[inline] + pub const fn as_micros(&self) -> u128 { + (self.0 / 1_000_000) as u128 + } + + /// Returns the number of whole milliseconds contained by this `Duration`. + #[must_use] + #[inline] + pub const fn as_millis(&self) -> u128 { + (self.0 / 1_000) as u128 + } + + /// Returns the total number of nanoseconds contained by this `Duration`. + #[must_use] + #[inline] + pub const fn as_nanos(&self) -> u128 { + self.0 as u128 + } } impl std::ops::Deref for Duration { diff --git a/test-programs/Cargo.toml b/test-programs/Cargo.toml index 0568427..633c274 100644 --- a/test-programs/Cargo.toml +++ b/test-programs/Cargo.toml @@ -8,4 +8,3 @@ publish = false futures-lite.workspace = true serde_json.workspace = true wstd.workspace = true -wasi.workspace = true From 032528f411eb62dee92968a3ddd01c440720b795 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 10 Jan 2025 16:54:14 -0800 Subject: [PATCH 06/19] Use `map`s. --- src/http/request.rs | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/http/request.rs b/src/http/request.rs index b34398d..34a16a5 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -63,26 +63,17 @@ pub fn try_from_incoming_request( let method = from_wasi_method(incoming.method()) .map_err(|_| WasiHttpErrorCode::HttpRequestMethodInvalid)?; - let scheme = match incoming.scheme() { - Some(scheme) => Some( - from_wasi_scheme(scheme).expect("TODO: what shall we do with an invalid uri here?"), - ), - None => None, - }; - let authority = match incoming.authority() { - Some(authority) => Some( - Authority::from_maybe_shared(authority) - .expect("TODO: what shall we do with an invalid uri authority here?"), - ), - None => None, - }; - let path_and_query = match incoming.path_with_query() { - Some(path_and_query) => Some( - PathAndQuery::from_maybe_shared(path_and_query) - .expect("TODO: what shall we do with an invalid uri path-and-query here?"), - ), - None => None, - }; + 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?") + }); // TODO: What's the right error code to use for invalid headers? let kind = BodyKind::from_headers(&headers) From 669de081fa8bb496e97ecca63ab85f4e1bb702a6 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 10 Jan 2025 17:01:43 -0800 Subject: [PATCH 07/19] Make the http_server example return a proper 404. --- examples/http_server.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/http_server.rs b/examples/http_server.rs index 3ee8b9b..f5e6041 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -1,6 +1,6 @@ use wstd::http::body::{BodyForthcoming, IncomingBody, OutgoingBody}; use wstd::http::server::{Finished, Responder}; -use wstd::http::{IntoBody, Request, Response}; +use wstd::http::{IntoBody, Request, Response, StatusCode}; use wstd::io::{copy, empty, AsyncWrite}; use wstd::time::{Duration, Instant}; @@ -13,7 +13,8 @@ async fn main(request: Request, responder: Responder) -> Finished "/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_home(request, responder).await, + _ => http_not_found(request, responder).await, } } @@ -83,3 +84,11 @@ async fn http_echo_trailers(request: Request, responder: Responder }; Finished::finish(body, result, trailers) } + +async fn http_not_found(_request: Request, responder: Responder) -> Finished { + let response = Response::builder() + .status(StatusCode::NOT_FOUND) + .body(empty()) + .unwrap(); + responder.respond(response).await +} From 5ce67143e61f44b796a35a35346c90e210a611c3 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 10 Jan 2025 17:01:58 -0800 Subject: [PATCH 08/19] Optimize away a `.clone()`. --- examples/http_server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/http_server.rs b/examples/http_server.rs index f5e6041..b21eda4 100644 --- a/examples/http_server.rs +++ b/examples/http_server.rs @@ -71,7 +71,7 @@ async fn http_bigfail(_request: Request, responder: Responder) -> async fn http_echo_headers(request: Request, responder: Responder) -> Finished { let mut response = Response::builder(); - *response.headers_mut().unwrap() = request.headers().clone(); + *response.headers_mut().unwrap() = request.into_parts().0.headers; let response = response.body(empty()).unwrap(); responder.respond(response).await } From 5a756c8c4200f2083b3d4b6e9916d91a4bde8347 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Fri, 10 Jan 2025 17:04:38 -0800 Subject: [PATCH 09/19] Disable default features in the ureq dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ce27bc2..cd173e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ syn = "2.0" test-log = { version = "0.2", features = ["trace"] } test-programs = { path = "test-programs" } test-programs-artifacts = { path = "test-programs/artifacts" } -ureq = "2.12.1" +ureq = { version = "2.12.1", default-features = false } wasi = "0.14.0" wasmtime = "26" wasmtime-cli = "26" From a8358de39eb627e22ac8a5e7add5307f121ac09f Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 13 Jan 2025 07:13:25 -0800 Subject: [PATCH 10/19] Spawn the wasmtime executable rather than using wasmtime_cli. --- Cargo.toml | 2 - test-programs/artifacts/Cargo.toml | 2 - test-programs/artifacts/tests/http_server.rs | 48 +++++++------------- 3 files changed, 16 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cd173e7..8019adc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,6 @@ authors = [ [workspace.dependencies] anyhow = "1" cargo_metadata = "0.18.1" -clap = { version = "4.5.23", features = ["derive"] } futures-core = "0.3.19" futures-lite = "1.12.0" heck = "0.5" @@ -66,7 +65,6 @@ test-programs-artifacts = { path = "test-programs/artifacts" } ureq = { version = "2.12.1", default-features = false } wasi = "0.14.0" wasmtime = "26" -wasmtime-cli = "26" wasmtime-wasi = "26" wasmtime-wasi-http = "26" wstd = { path = "." } diff --git a/test-programs/artifacts/Cargo.toml b/test-programs/artifacts/Cargo.toml index 152ac54..d35044a 100644 --- a/test-programs/artifacts/Cargo.toml +++ b/test-programs/artifacts/Cargo.toml @@ -9,12 +9,10 @@ publish = false [dev-dependencies] anyhow.workspace = true -clap.workspace = true test-log.workspace = true test-programs-artifacts.workspace = true ureq.workspace = true wasmtime.workspace = true -wasmtime-cli.workspace = true wasmtime-wasi.workspace = true wasmtime-wasi-http.workspace = true diff --git a/test-programs/artifacts/tests/http_server.rs b/test-programs/artifacts/tests/http_server.rs index 8303e99..995298b 100644 --- a/test-programs/artifacts/tests/http_server.rs +++ b/test-programs/artifacts/tests/http_server.rs @@ -1,25 +1,5 @@ use anyhow::Result; - -fn run_in_wasmtime(wasm: &str) -> Result<()> { - use clap::Parser; - use wasmtime_cli::commands::ServeCommand; - - // Run wasmtime serve. - // Enable -Scli because we build with the default adapter rather than the - // proxy adapter. - // Disable logging so that Wasmtime's tracing_subscriber registration - // doesn't conflict with the test harness' registration. - let serve = - match ServeCommand::try_parse_from(["serve", "-Scli", "-Dlogging=n", wasm].into_iter()) { - Ok(serve) => serve, - Err(e) => { - dbg!(&e); - return Err(e.into()); - } - }; - - serve.execute() -} +use std::process::Command; #[test_log::test] fn http_server() -> Result<()> { @@ -27,24 +7,30 @@ fn http_server() -> Result<()> { use std::thread::sleep; use std::time::Duration; - // Start a `wasmtime serve` server. - let wasmtime_thread = - std::thread::spawn(move || run_in_wasmtime(test_programs_artifacts::HTTP_SERVER)); + // 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()?; // Clumsily wait for the server to accept connections. 'wait: loop { sleep(Duration::from_millis(100)); - if TcpStream::connect("127.0.0.1:8080").is_ok() { + if TcpStream::connect("127.0.0.1:8081").is_ok() { break 'wait; } } // Do some tests! - let body: String = ureq::get("http://127.0.0.1:8080").call()?.into_string()?; + 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:8080/fail").call() { + match ureq::get("http://127.0.0.1:8081/fail").call() { Ok(body) => { unreachable!("unexpected success from /fail: {:?}", body); } @@ -56,7 +42,7 @@ fn http_server() -> Result<()> { const MESSAGE: &[u8] = b"hello, echoserver!\n"; - let body: String = ureq::get("http://127.0.0.1:8080/echo") + let body: String = ureq::get("http://127.0.0.1:8081/echo") .send(MESSAGE)? .into_string()?; assert_eq!(body.as_bytes(), MESSAGE); @@ -70,7 +56,7 @@ fn http_server() -> Result<()> { ("Purple", "Beets"), ]; - let mut response = ureq::get("http://127.0.0.1:8080/echo-headers"); + let mut response = ureq::get("http://127.0.0.1:8081/echo-headers"); for (name, value) in test_headers { response = response.set(name, value); } @@ -81,9 +67,7 @@ fn http_server() -> Result<()> { assert_eq!(response.header(name), Some(value)); } - if wasmtime_thread.is_finished() { - wasmtime_thread.join().expect("wasmtime panicked")?; - } + wasmtime_process.kill()?; Ok(()) } From 257ca198a5ffca30b7e7d698503b87e3c6c494c1 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 13 Jan 2025 07:13:51 -0800 Subject: [PATCH 11/19] Remove `allow(dead_code)` because it seems rustc doesn't warn. --- macro/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 9c19e06..5b8c8e5 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -150,7 +150,6 @@ pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStr // In case the user needs it, provide a `main` function so that the // code compiles. - #[allow(dead_code)] fn main() { unreachable!("HTTP-server components should be run wth `handle` rather than `main`") } } .into() From 452513636faa396726a9086e16fbce6dad4170c5 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 13 Jan 2025 07:14:43 -0800 Subject: [PATCH 12/19] Rewrite the comment about the generated main function. --- macro/src/lib.rs | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/macro/src/lib.rs b/macro/src/lib.rs index 5b8c8e5..f9ce630 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -84,7 +84,7 @@ pub fn attr_macro_test(_attr: TokenStream, item: TokenStream) -> TokenStream { .into() } -/// Enables a HTTP-server main function, for creating [HTTP servers]. +/// Enables a HTTP server main function, for creating [HTTP servers]. /// /// [HTTP servers]: https://docs.rs/wstd/latest/wstd/http/server/index.html /// @@ -148,9 +148,38 @@ pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStr ::wstd::wasi::http::proxy::export!(TheServer with_types_in ::wstd::wasi); - // In case the user needs it, provide a `main` function so that the - // code compiles. - fn main() { unreachable!("HTTP-server components should be run wth `handle` rather than `main`") } + // Provide an actual function named `main`. + // + // WASI HTTP server components don't use a traditional `main` function. + // They export a function named `handle` which takes a `Request` + // argument, and which may be called multiple times on the same + // instance. To let users write a familiar `fn main` in a file + // named src/main.rs, we provide this `wstd::main` macro, which + // transforms the user's `fn main` into the appropriate `handle` + // function. + // + // However, when the top-level file is named src/main.rs, rustc + // requires there to be a function named `main` somewhere in it. This + // requirement can be disabled using `#![no_main]`, however we can't + // use that automatically because macros can't contain inner + // attributes, and we don't want to require users to add `#![no_main]` + // in their own code. + // + // So, we include a definition of a function named `main` here, which + // isn't intended to ever be called, and exists just to satify the + // requirement for a `main` function. + // + // Users could use `#![no_main]` if they want to. Or, they could name + // their top-level file src/lib.rs and add + // ```toml + // [lib] + // crate-type = ["cdylib"] + // ``` + // to their Cargo.toml. With either of these, this "main" function will + // be ignored as dead code. + fn main() { + unreachable!("HTTP server components should be run with `handle` rather than `run`") + } } .into() } From 907e883f5c0a93a4b73625cfe789b1dfd6f5fe68 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 13 Jan 2025 07:16:14 -0800 Subject: [PATCH 13/19] Delete `InvalidHeader`, which was unused. --- src/http/fields.rs | 41 ----------------------------------------- 1 file changed, 41 deletions(-) diff --git a/src/http/fields.rs b/src/http/fields.rs index 2727468..89f95d9 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -1,9 +1,6 @@ pub use http::header::{HeaderMap, HeaderName, HeaderValue}; -use http::header::{InvalidHeaderName, InvalidHeaderValue}; -use super::error::ErrorVariant; use super::{Error, Result}; -use std::fmt; use wasi::http::types::Fields; pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { @@ -28,41 +25,3 @@ pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Fields { } wasi_fields } - -#[derive(Debug)] -pub(crate) enum InvalidHeader { - Name(InvalidHeaderName), - Value(InvalidHeaderValue), -} - -impl fmt::Display for InvalidHeader { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Name(e) => e.fmt(f), - Self::Value(e) => e.fmt(f), - } - } -} - -impl std::error::Error for InvalidHeader {} - -impl From for InvalidHeader { - fn from(e: InvalidHeaderName) -> Self { - Self::Name(e) - } -} - -impl From for InvalidHeader { - fn from(e: InvalidHeaderValue) -> Self { - Self::Value(e) - } -} - -impl From for Error { - fn from(e: InvalidHeader) -> Self { - match e { - InvalidHeader::Name(e) => ErrorVariant::HeaderName(e).into(), - InvalidHeader::Value(e) => ErrorVariant::HeaderValue(e).into(), - } - } -} From 8bf107bceea15a20b6edf367d0e5acf7b15837b8 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 13 Jan 2025 07:17:34 -0800 Subject: [PATCH 14/19] Fix units in `Instant::duration_since`. --- src/time/instant.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/time/instant.rs b/src/time/instant.rs index b9db4b9..02c87f7 100644 --- a/src/time/instant.rs +++ b/src/time/instant.rs @@ -30,7 +30,7 @@ impl Instant { /// Returns the amount of time elapsed from another instant to this one, or zero duration if /// that instant is later than this one. pub fn duration_since(&self, earlier: Instant) -> Duration { - Duration::from_micros(self.0.checked_sub(earlier.0).unwrap_or_default()) + Duration::from_nanos(self.0.checked_sub(earlier.0).unwrap_or_default()) } /// Returns the amount of time elapsed since this instant. @@ -90,3 +90,16 @@ impl IntoFuture for Instant { crate::task::sleep_until(self) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_duration_since() { + let x = Instant::now(); + let d = Duration::new(456, 789); + let y = x + d; + assert_eq!(y.duration_since(x), d); + } +} From 1af6d5848b4fbecf5fcbf5c172142c660deefa62 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 13 Jan 2025 07:27:05 -0800 Subject: [PATCH 15/19] Fix unit conversions in `Duration::as_micros` and `as_millis`. --- src/time/duration.rs | 68 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/src/time/duration.rs b/src/time/duration.rs index 3571d4c..700b244 100644 --- a/src/time/duration.rs +++ b/src/time/duration.rs @@ -41,6 +41,13 @@ impl Duration { std::time::Duration::from_micros(micros).into() } + /// Creates a new `Duration` from the specified number of nanoseconds. + #[must_use] + #[inline] + pub fn from_nanos(nanos: u64) -> Self { + std::time::Duration::from_nanos(nanos).into() + } + /// Creates a new `Duration` from the specified number of seconds represented /// as `f64`. /// @@ -78,17 +85,17 @@ impl Duration { self.0 / 1_000_000_000 } - /// Returns the number of whole microseconds contained by this `Duration`. + /// Returns the number of whole milliseconds contained by this `Duration`. #[must_use] #[inline] - pub const fn as_micros(&self) -> u128 { + pub const fn as_millis(&self) -> u128 { (self.0 / 1_000_000) as u128 } - /// Returns the number of whole milliseconds contained by this `Duration`. + /// Returns the number of whole microseconds contained by this `Duration`. #[must_use] #[inline] - pub const fn as_millis(&self) -> u128 { + pub const fn as_micros(&self) -> u128 { (self.0 / 1_000) as u128 } @@ -168,3 +175,56 @@ impl IntoFuture for Duration { crate::task::sleep(self) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_from_as() { + assert_eq!(Duration::new(456, 864209753).as_secs(), 456); + assert_eq!(Duration::new(456, 864209753).as_millis(), 456864); + assert_eq!(Duration::new(456, 864209753).as_micros(), 456864209); + assert_eq!(Duration::new(456, 864209753).as_nanos(), 456864209753); + + assert_eq!(Duration::from_secs(9876543210).as_secs(), 9876543210); + assert_eq!(Duration::from_secs(9876543210).as_millis(), 9876543210_000); + assert_eq!( + Duration::from_secs(9876543210).as_micros(), + 9876543210_000000 + ); + assert_eq!( + Duration::from_secs(9876543210).as_nanos(), + 9876543210_000000000 + ); + + assert_eq!(Duration::from_millis(9876543210).as_secs(), 9876543); + assert_eq!(Duration::from_millis(9876543210).as_millis(), 9876543210); + assert_eq!( + Duration::from_millis(9876543210).as_micros(), + 9876543210_000 + ); + assert_eq!( + Duration::from_millis(9876543210).as_nanos(), + 9876543210_000000 + ); + + assert_eq!(Duration::from_micros(9876543210).as_secs(), 9876); + assert_eq!(Duration::from_micros(9876543210).as_millis(), 9876543); + assert_eq!(Duration::from_micros(9876543210).as_micros(), 9876543210); + assert_eq!(Duration::from_micros(9876543210).as_nanos(), 9876543210_000); + + assert_eq!(Duration::from_nanos(9876543210).as_secs(), 9); + assert_eq!(Duration::from_nanos(9876543210).as_millis(), 9876); + assert_eq!(Duration::from_nanos(9876543210).as_micros(), 9876543); + assert_eq!(Duration::from_nanos(9876543210).as_nanos(), 9876543210); + } + + #[test] + fn test_from_secs_float() { + assert_eq!(Duration::from_secs_f64(158.9).as_secs(), 158); + assert_eq!(Duration::from_secs_f32(158.9).as_secs(), 158); + assert_eq!(Duration::from_secs_f64(159.1).as_secs(), 159); + assert_eq!(Duration::from_secs_f32(159.1).as_secs(), 159); + } +} From e44c1cab69a09f1749d4a7edd58e073a6fb81472 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 13 Jan 2025 07:31:12 -0800 Subject: [PATCH 16/19] If a server has an I/O error writing the body, panic. --- src/http/server.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/http/server.rs b/src/http/server.rs index 71c3481..ba4a9c5 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -170,15 +170,13 @@ impl Finished { // it's dropped first. drop(stream); - if result.is_ok() { - let wasi_trailers = trailers.map(|trailers| header_map_to_wasi(&trailers)); - - wasi::http::types::OutgoingBody::finish(body, wasi_trailers) - .expect("body length did not match Content-Length header value"); - } else { - // As in `fail`, there's no need to do anything else on failure. - // TODO: Should we log the failure somewhere? - } + // If there was an I/O error, panic and don't call `OutgoingBody::finish`. + let _ = result.expect("I/O error while writing the body"); + + let wasi_trailers = trailers.map(|trailers| header_map_to_wasi(&trailers)); + + wasi::http::types::OutgoingBody::finish(body, wasi_trailers) + .expect("body length did not match Content-Length header value"); Self(()) } From 6eebc836810dd101bbe23223e0c1f130b190a6c2 Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 13 Jan 2025 07:34:41 -0800 Subject: [PATCH 17/19] Make `header_map_to_wasi` bubble up its error. The client can bubble this into a `wstd::http::Error`. The server currently just does `.expect`, but at least now, if we want it to do something different, it's more clear what the options are. --- src/http/error.rs | 10 +++++++--- src/http/fields.rs | 21 +++++++++++++++------ src/http/request.rs | 2 +- src/http/server.rs | 7 ++++--- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/http/error.rs b/src/http/error.rs index a32cf1c..bfa5c36 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -1,3 +1,4 @@ +use crate::http::fields::ToWasiHeaderError; use std::fmt; /// The `http` result type. @@ -78,9 +79,12 @@ impl From for Error { } } -impl From for Error { - fn from(e: WasiHttpHeaderError) -> Error { - ErrorVariant::WasiHeader(e).into() +impl From for Error { + fn from(error: ToWasiHeaderError) -> Error { + Error { + variant: ErrorVariant::WasiHeader(error.error), + context: vec![error.context], + } } } diff --git a/src/http/fields.rs b/src/http/fields.rs index 89f95d9..1683e1f 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -1,9 +1,9 @@ pub use http::header::{HeaderMap, HeaderName, HeaderValue}; -use super::{Error, Result}; -use wasi::http::types::Fields; +use super::Error; +use wasi::http::types::{Fields, HeaderError as WasiHttpHeaderError}; -pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { +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()) @@ -15,13 +15,22 @@ pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { Ok(output) } -pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Fields { +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()) - .unwrap_or_else(|err| panic!("header named {key}: {err:?}")); + .map_err(|error| ToWasiHeaderError { + error, + context: format!("header {key}: {value:?}"), + })?; } - wasi_fields + Ok(wasi_fields) +} + +#[derive(Debug)] +pub(crate) struct ToWasiHeaderError { + pub(crate) error: WasiHttpHeaderError, + pub(crate) context: String, } diff --git a/src/http/request.rs b/src/http/request.rs index 34a16a5..e4b0c5a 100644 --- a/src/http/request.rs +++ b/src/http/request.rs @@ -13,7 +13,7 @@ use wasi::http::types::IncomingRequest; pub use http::Request; pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingRequest, T), Error> { - let wasi_req = OutgoingRequest::new(header_map_to_wasi(request.headers())); + let wasi_req = OutgoingRequest::new(header_map_to_wasi(request.headers())?); let (parts, body) = request.into_parts(); diff --git a/src/http/server.rs b/src/http/server.rs index ba4a9c5..3e4ce56 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -61,7 +61,7 @@ impl Responder { /// # } /// ``` pub fn start_response(self, response: Response) -> OutgoingBody { - let wasi_headers = header_map_to_wasi(response.headers()); + 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(); @@ -100,7 +100,7 @@ impl Responder { let headers = response.headers(); let status = response.status().as_u16(); - let wasi_headers = header_map_to_wasi(headers); + 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(); @@ -173,7 +173,8 @@ impl Finished { // If there was an I/O error, panic and don't call `OutgoingBody::finish`. let _ = result.expect("I/O error while writing the body"); - let wasi_trailers = trailers.map(|trailers| header_map_to_wasi(&trailers)); + let wasi_trailers = + trailers.map(|trailers| header_map_to_wasi(&trailers).expect("header error")); wasi::http::types::OutgoingBody::finish(body, wasi_trailers) .expect("body length did not match Content-Length header value"); From 7142798761908900418d65d731d92453e0792c6f Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 13 Jan 2025 07:55:34 -0800 Subject: [PATCH 18/19] Fix some clippy lints. --- src/http/fields.rs | 2 +- src/http/request.rs | 2 +- src/http/server.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/http/fields.rs b/src/http/fields.rs index 1683e1f..cd41684 100644 --- a/src/http/fields.rs +++ b/src/http/fields.rs @@ -20,7 +20,7 @@ pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Result(request: Request) -> Result<(OutgoingReque // 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())) + .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:?}")) })?; diff --git a/src/http/server.rs b/src/http/server.rs index 3e4ce56..2d2400e 100644 --- a/src/http/server.rs +++ b/src/http/server.rs @@ -109,7 +109,7 @@ impl Responder { if let Some(len) = body.len() { let mut buffer = itoa::Buffer::new(); wasi_headers - .append(&CONTENT_LENGTH.as_str(), &buffer.format(len).as_bytes()) + .append(CONTENT_LENGTH.as_str(), buffer.format(len).as_bytes()) .unwrap(); } @@ -171,7 +171,7 @@ impl Finished { drop(stream); // If there was an I/O error, panic and don't call `OutgoingBody::finish`. - let _ = result.expect("I/O error while writing the body"); + result.expect("I/O error while writing the body"); let wasi_trailers = trailers.map(|trailers| header_map_to_wasi(&trailers).expect("header error")); From 5b1955b679ff78c31ed1bbdb83f345142b66d0ff Mon Sep 17 00:00:00 2001 From: Dan Gohman Date: Mon, 13 Jan 2025 09:46:42 -0800 Subject: [PATCH 19/19] Update a comment to name the correct macro. --- macro/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macro/src/lib.rs b/macro/src/lib.rs index f9ce630..fa78ee0 100644 --- a/macro/src/lib.rs +++ b/macro/src/lib.rs @@ -154,7 +154,7 @@ pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStr // They export a function named `handle` which takes a `Request` // argument, and which may be called multiple times on the same // instance. To let users write a familiar `fn main` in a file - // named src/main.rs, we provide this `wstd::main` macro, which + // named src/main.rs, we provide this `wstd::http_server` macro, which // transforms the user's `fn main` into the appropriate `handle` // function. //