Skip to content

Commit 083dc21

Browse files
committed
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`.
1 parent 94a0844 commit 083dc21

File tree

13 files changed

+605
-21
lines changed

13 files changed

+605
-21
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ categories.workspace = true
1515
[dependencies]
1616
futures-core.workspace = true
1717
http.workspace = true
18+
itoa.workspace = true
1819
pin-project-lite.workspace = true
1920
slab.workspace = true
2021
wasi.workspace = true
@@ -52,6 +53,7 @@ futures-core = "0.3.19"
5253
futures-lite = "1.12.0"
5354
heck = "0.5"
5455
http = "1.1"
56+
itoa = "1"
5557
pin-project-lite = "0.2.8"
5658
quote = "1.0"
5759
serde_json = "1"

examples/http_server.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use wstd::http::body::{BodyForthcoming, IncomingBody, OutgoingBody};
2+
use wstd::http::server::{Finished, Responder};
3+
use wstd::http::{IntoBody, Request, Response};
4+
use wstd::io::{copy, AsyncWrite};
5+
6+
#[wstd::http_server]
7+
async fn main(request: Request<IncomingBody>, responder: Responder) -> Finished {
8+
match request.uri().path_and_query().unwrap().as_str() {
9+
"/wait" => http_wait(request, responder).await,
10+
"/echo" => http_echo(request, responder).await,
11+
"/fail" => http_fail(request, responder).await,
12+
"/bigfail" => http_bigfail(request, responder).await,
13+
"/" | _ => http_home(request, responder).await,
14+
}
15+
}
16+
17+
async fn http_home(_request: Request<IncomingBody>, responder: Responder) -> Finished {
18+
// To send a single string as the response body, use `Responder::respond`.
19+
responder
20+
.respond(Response::new("Hello, wasi:http/proxy world!\n".into_body()))
21+
.await
22+
}
23+
24+
async fn http_wait(_request: Request<IncomingBody>, responder: Responder) -> Finished {
25+
// Get the time now
26+
let now = wasi::clocks::monotonic_clock::now();
27+
28+
// Sleep for 1 second
29+
let nanos = 1_000_000_000;
30+
let pollable = wasi::clocks::monotonic_clock::subscribe_duration(nanos);
31+
pollable.block();
32+
33+
// Compute how long we slept for.
34+
let elapsed = wasi::clocks::monotonic_clock::now() - now;
35+
let elapsed = elapsed / 1_000_000; // change to millis
36+
37+
// To stream data to the response body, use `Responder::start_response`.
38+
let mut body = responder.start_response(Response::new(BodyForthcoming));
39+
let result = body
40+
.write_all(format!("slept for {elapsed} millis\n").as_bytes())
41+
.await;
42+
Finished::finish(body, result, None)
43+
}
44+
45+
async fn http_echo(mut request: Request<IncomingBody>, responder: Responder) -> Finished {
46+
// Stream data from the request body to the response body.
47+
let mut body = responder.start_response(Response::new(BodyForthcoming));
48+
let result = copy(request.body_mut(), &mut body).await;
49+
Finished::finish(body, result, None)
50+
}
51+
52+
async fn http_fail(_request: Request<IncomingBody>, responder: Responder) -> Finished {
53+
let body = responder.start_response(Response::new(BodyForthcoming));
54+
Finished::fail(body)
55+
}
56+
57+
async fn http_bigfail(_request: Request<IncomingBody>, responder: Responder) -> Finished {
58+
async fn write_body(body: &mut OutgoingBody) -> wstd::io::Result<()> {
59+
for _ in 0..0x10 {
60+
body.write_all("big big big big\n".as_bytes()).await?;
61+
}
62+
body.flush().await?;
63+
Ok(())
64+
}
65+
66+
let mut body = responder.start_response(Response::new(BodyForthcoming));
67+
let _ = write_body(&mut body).await;
68+
Finished::fail(body)
69+
}

macro/src/lib.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,75 @@ pub fn attr_macro_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
8383
}
8484
.into()
8585
}
86+
87+
/// Enables a HTTP-server main function, for creating [HTTP servers].
88+
///
89+
/// [HTTP servers]: https://docs.rs/wstd/latest/wstd/http/server/index.html
90+
///
91+
/// # Examples
92+
///
93+
/// ```ignore
94+
/// #[wstd::http_server]
95+
/// async fn main(request: Request<IncomingBody>, responder: Responder) -> Finished {
96+
/// responder
97+
/// .respond(Response::new("Hello!\n".into_body()))
98+
/// .await
99+
/// }
100+
/// ```
101+
#[proc_macro_attribute]
102+
pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStream {
103+
let input = parse_macro_input!(item as ItemFn);
104+
105+
if input.sig.asyncness.is_none() {
106+
return quote_spanned! { input.sig.fn_token.span()=>
107+
compile_error!("fn must be `async fn`");
108+
}
109+
.into();
110+
}
111+
112+
let output = &input.sig.output;
113+
let inputs = &input.sig.inputs;
114+
let name = &input.sig.ident;
115+
let body = &input.block;
116+
let attrs = &input.attrs;
117+
let vis = &input.vis;
118+
119+
if name != "main" {
120+
return quote_spanned! { input.sig.ident.span()=>
121+
compile_error!("only `async fn main` can be used for #[wstd::http_server]");
122+
}
123+
.into();
124+
}
125+
126+
quote! {
127+
struct TheServer;
128+
129+
impl ::wstd::wasi::exports::http::incoming_handler::Guest for TheServer {
130+
fn handle(
131+
request: ::wstd::wasi::http::types::IncomingRequest,
132+
response_out: ::wstd::wasi::http::types::ResponseOutparam
133+
) {
134+
#(#attrs)*
135+
#vis async fn __run(#inputs) #output {
136+
#body
137+
}
138+
139+
let responder = ::wstd::http::server::Responder::new(response_out);
140+
let _finished: ::wstd::http::server::Finished =
141+
match ::wstd::http::try_from_incoming_request(request)
142+
{
143+
Ok(request) => ::wstd::runtime::block_on(async { __run(request, responder).await }),
144+
Err(err) => responder.fail(err),
145+
};
146+
}
147+
}
148+
149+
::wstd::wasi::http::proxy::export!(TheServer with_types_in ::wstd::wasi);
150+
151+
// In case the user needs it, provide a `main` function so that the
152+
// code compiles.
153+
#[allow(dead_code)]
154+
fn main() { unreachable!("HTTP-server components should be run wth `handle` rather than `main`") }
155+
}
156+
.into()
157+
}

src/http/body.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! HTTP body types
22
3-
use crate::io::{AsyncInputStream, AsyncRead, Cursor, Empty};
3+
use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Cursor, Empty};
44
use core::fmt;
55
use http::header::{CONTENT_LENGTH, TRANSFER_ENCODING};
66
use wasi::http::types::IncomingBody as WasiIncomingBody;
@@ -177,3 +177,79 @@ impl From<InvalidContentLength> for Error {
177177
ErrorVariant::Other(e.to_string()).into()
178178
}
179179
}
180+
181+
/// The output stream for the body, implementing [`AsyncWrite`]. Call
182+
/// [`Responder::start_response`] to obtain one. Once the body is complete,
183+
/// it must be declared finished, using [`OutgoingBody::finish`].
184+
#[must_use]
185+
pub struct OutgoingBody {
186+
// IMPORTANT: the order of these fields here matters. `stream` must
187+
// be dropped before `body`.
188+
stream: AsyncOutputStream,
189+
body: wasi::http::types::OutgoingBody,
190+
dontdrop: DontDropOutgoingBody,
191+
}
192+
193+
impl OutgoingBody {
194+
pub(crate) fn new(stream: AsyncOutputStream, body: wasi::http::types::OutgoingBody) -> Self {
195+
Self {
196+
stream,
197+
body,
198+
dontdrop: DontDropOutgoingBody,
199+
}
200+
}
201+
202+
pub(crate) fn consume(self) -> (AsyncOutputStream, wasi::http::types::OutgoingBody) {
203+
let Self {
204+
stream,
205+
body,
206+
dontdrop,
207+
} = self;
208+
209+
std::mem::forget(dontdrop);
210+
211+
(stream, body)
212+
}
213+
214+
/// Return a reference to the underlying `AsyncOutputStream`.
215+
///
216+
/// This usually isn't needed, as `OutgoingBody` implements `AsyncWrite`
217+
/// too, however it is useful for code that expects to work with
218+
/// `AsyncOutputStream` specifically.
219+
pub fn stream(&mut self) -> &mut AsyncOutputStream {
220+
&mut self.stream
221+
}
222+
}
223+
224+
impl AsyncWrite for OutgoingBody {
225+
async fn write(&mut self, buf: &[u8]) -> crate::io::Result<usize> {
226+
self.stream.write(buf).await
227+
}
228+
229+
async fn flush(&mut self) -> crate::io::Result<()> {
230+
self.stream.flush().await
231+
}
232+
233+
fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> {
234+
Some(&self.stream)
235+
}
236+
}
237+
238+
/// A utility to ensure that `OutgoingBody` is either finished or failed, and
239+
/// not implicitly dropped.
240+
struct DontDropOutgoingBody;
241+
242+
impl Drop for DontDropOutgoingBody {
243+
fn drop(&mut self) {
244+
unreachable!("`OutgoingBody::drop` called; `OutgoingBody`s should be consumed with `finish` or `fail`.");
245+
}
246+
}
247+
248+
/// A placeholder for use as the type parameter to [`Response`] to indicate
249+
/// that the body has not yet started. This is used with
250+
/// [`Responder::start_response`], which has a `Response<BodyForthcoming>`
251+
/// argument.
252+
///
253+
/// To instead start the response and obtain the output stream for the body,
254+
/// use [`Responder::respond`].
255+
pub struct BodyForthcoming;

src/http/client.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ impl Client {
2020

2121
/// Send an HTTP request.
2222
pub async fn send<B: Body>(&self, req: Request<B>) -> Result<Response<IncomingBody>> {
23+
// We don't use `body::OutputBody` here because we can report I/O
24+
// errors from the `copy` directly.
2325
let (wasi_req, body) = try_into_outgoing(req)?;
2426
let wasi_body = wasi_req.body().unwrap();
2527
let body_stream = wasi_body.write().unwrap();

src/http/fields.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
pub use http::header::{HeaderMap, HeaderName, HeaderValue};
2+
use http::header::{InvalidHeaderName, InvalidHeaderValue};
23

4+
use super::error::ErrorVariant;
35
use super::{Error, Result};
6+
use std::fmt;
47
use wasi::http::types::Fields;
58

69
pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result<HeaderMap> {
@@ -15,12 +18,52 @@ pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result<HeaderMap> {
1518
Ok(output)
1619
}
1720

18-
pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Result<Fields> {
21+
pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Fields {
1922
let wasi_fields = Fields::new();
2023
for (key, value) in header_map {
24+
// Unwrap because `HeaderMap` has already validated the headers.
25+
// TODO: Remove the `to_owned()` calls after bytecodealliance/wit-bindgen#1102.
2126
wasi_fields
2227
.append(&key.as_str().to_owned(), &value.as_bytes().to_owned())
23-
.map_err(|e| Error::from(e).context("header named {key}"))?;
28+
.unwrap_or_else(|err| panic!("header named {key}: {err:?}"));
29+
}
30+
wasi_fields
31+
}
32+
33+
#[derive(Debug)]
34+
pub(crate) enum InvalidHeader {
35+
Name(InvalidHeaderName),
36+
Value(InvalidHeaderValue),
37+
}
38+
39+
impl fmt::Display for InvalidHeader {
40+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41+
match self {
42+
Self::Name(e) => e.fmt(f),
43+
Self::Value(e) => e.fmt(f),
44+
}
45+
}
46+
}
47+
48+
impl std::error::Error for InvalidHeader {}
49+
50+
impl From<InvalidHeaderName> for InvalidHeader {
51+
fn from(e: InvalidHeaderName) -> Self {
52+
Self::Name(e)
53+
}
54+
}
55+
56+
impl From<InvalidHeaderValue> for InvalidHeader {
57+
fn from(e: InvalidHeaderValue) -> Self {
58+
Self::Value(e)
59+
}
60+
}
61+
62+
impl From<InvalidHeader> for Error {
63+
fn from(e: InvalidHeader) -> Self {
64+
match e {
65+
InvalidHeader::Name(e) => ErrorVariant::HeaderName(e).into(),
66+
InvalidHeader::Value(e) => ErrorVariant::HeaderValue(e).into(),
67+
}
2468
}
25-
Ok(wasi_fields)
2669
}

src/http/method.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use wasi::http::types::Method as WasiMethod;
22

3-
use super::Result;
3+
use http::method::InvalidMethod;
44
pub use http::Method;
55

66
pub(crate) fn to_wasi_method(value: Method) -> WasiMethod {
@@ -18,9 +18,7 @@ pub(crate) fn to_wasi_method(value: Method) -> WasiMethod {
1818
}
1919
}
2020

21-
// This will become useful once we support IncomingRequest
22-
#[allow(dead_code)]
23-
pub(crate) fn from_wasi_method(value: WasiMethod) -> Result<Method> {
21+
pub(crate) fn from_wasi_method(value: WasiMethod) -> Result<Method, InvalidMethod> {
2422
Ok(match value {
2523
WasiMethod::Get => Method::GET,
2624
WasiMethod::Head => Method::HEAD,

src/http/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
//! HTTP networking support
22
//!
33
pub use http::status::StatusCode;
4-
pub use http::uri::Uri;
4+
pub use http::uri::{Authority, PathAndQuery, Uri};
55

66
#[doc(inline)]
77
pub use body::{Body, IntoBody};
88
pub use client::Client;
99
pub use error::{Error, Result};
1010
pub use fields::{HeaderMap, HeaderName, HeaderValue};
1111
pub use method::Method;
12-
pub use request::Request;
12+
pub use request::{try_from_incoming_request, Request};
1313
pub use response::Response;
14+
pub use scheme::{InvalidUri, Scheme};
1415

1516
pub mod body;
1617

@@ -20,3 +21,5 @@ mod fields;
2021
mod method;
2122
mod request;
2223
mod response;
24+
mod scheme;
25+
pub mod server;

0 commit comments

Comments
 (0)