Skip to content

Commit 35f28d2

Browse files
authored
Merge pull request #52 from sunfishcode/sunfishcode/http-server
Implement an HTTP server framework.
2 parents 94a0844 + 5b1955b commit 35f28d2

File tree

19 files changed

+824
-32
lines changed

19 files changed

+824
-32
lines changed

Cargo.toml

Lines changed: 4 additions & 1 deletion
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"
@@ -60,7 +62,8 @@ syn = "2.0"
6062
test-log = { version = "0.2", features = ["trace"] }
6163
test-programs = { path = "test-programs" }
6264
test-programs-artifacts = { path = "test-programs/artifacts" }
63-
wasi = "0.13.1"
65+
ureq = { version = "2.12.1", default-features = false }
66+
wasi = "0.14.0"
6467
wasmtime = "26"
6568
wasmtime-wasi = "26"
6669
wasmtime-wasi-http = "26"

examples/http_server.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use wstd::http::body::{BodyForthcoming, IncomingBody, OutgoingBody};
2+
use wstd::http::server::{Finished, Responder};
3+
use wstd::http::{IntoBody, Request, Response, StatusCode};
4+
use wstd::io::{copy, empty, AsyncWrite};
5+
use wstd::time::{Duration, Instant};
6+
7+
#[wstd::http_server]
8+
async fn main(request: Request<IncomingBody>, responder: Responder) -> Finished {
9+
match request.uri().path_and_query().unwrap().as_str() {
10+
"/wait" => http_wait(request, responder).await,
11+
"/echo" => http_echo(request, responder).await,
12+
"/echo-headers" => http_echo_headers(request, responder).await,
13+
"/echo-trailers" => http_echo_trailers(request, responder).await,
14+
"/fail" => http_fail(request, responder).await,
15+
"/bigfail" => http_bigfail(request, responder).await,
16+
"/" => http_home(request, responder).await,
17+
_ => http_not_found(request, responder).await,
18+
}
19+
}
20+
21+
async fn http_home(_request: Request<IncomingBody>, responder: Responder) -> Finished {
22+
// To send a single string as the response body, use `Responder::respond`.
23+
responder
24+
.respond(Response::new("Hello, wasi:http/proxy world!\n".into_body()))
25+
.await
26+
}
27+
28+
async fn http_wait(_request: Request<IncomingBody>, responder: Responder) -> Finished {
29+
// Get the time now
30+
let now = Instant::now();
31+
32+
// Sleep for one second.
33+
wstd::task::sleep(Duration::from_secs(1)).await;
34+
35+
// Compute how long we slept for.
36+
let elapsed = Instant::now().duration_since(now).as_millis();
37+
38+
// To stream data to the response body, use `Responder::start_response`.
39+
let mut body = responder.start_response(Response::new(BodyForthcoming));
40+
let result = body
41+
.write_all(format!("slept for {elapsed} millis\n").as_bytes())
42+
.await;
43+
Finished::finish(body, result, None)
44+
}
45+
46+
async fn http_echo(mut request: Request<IncomingBody>, responder: Responder) -> Finished {
47+
// Stream data from the request body to the response body.
48+
let mut body = responder.start_response(Response::new(BodyForthcoming));
49+
let result = copy(request.body_mut(), &mut body).await;
50+
Finished::finish(body, result, None)
51+
}
52+
53+
async fn http_fail(_request: Request<IncomingBody>, responder: Responder) -> Finished {
54+
let body = responder.start_response(Response::new(BodyForthcoming));
55+
Finished::fail(body)
56+
}
57+
58+
async fn http_bigfail(_request: Request<IncomingBody>, responder: Responder) -> Finished {
59+
async fn write_body(body: &mut OutgoingBody) -> wstd::io::Result<()> {
60+
for _ in 0..0x10 {
61+
body.write_all("big big big big\n".as_bytes()).await?;
62+
}
63+
body.flush().await?;
64+
Ok(())
65+
}
66+
67+
let mut body = responder.start_response(Response::new(BodyForthcoming));
68+
let _ = write_body(&mut body).await;
69+
Finished::fail(body)
70+
}
71+
72+
async fn http_echo_headers(request: Request<IncomingBody>, responder: Responder) -> Finished {
73+
let mut response = Response::builder();
74+
*response.headers_mut().unwrap() = request.into_parts().0.headers;
75+
let response = response.body(empty()).unwrap();
76+
responder.respond(response).await
77+
}
78+
79+
async fn http_echo_trailers(request: Request<IncomingBody>, responder: Responder) -> Finished {
80+
let body = responder.start_response(Response::new(BodyForthcoming));
81+
let (trailers, result) = match request.into_body().finish().await {
82+
Ok(trailers) => (trailers, Ok(())),
83+
Err(err) => (Default::default(), Err(std::io::Error::other(err))),
84+
};
85+
Finished::finish(body, result, trailers)
86+
}
87+
88+
async fn http_not_found(_request: Request<IncomingBody>, responder: Responder) -> Finished {
89+
let response = Response::builder()
90+
.status(StatusCode::NOT_FOUND)
91+
.body(empty())
92+
.unwrap();
93+
responder.respond(response).await
94+
}

macro/src/lib.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,103 @@ 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+
// Provide an actual function named `main`.
152+
//
153+
// WASI HTTP server components don't use a traditional `main` function.
154+
// They export a function named `handle` which takes a `Request`
155+
// argument, and which may be called multiple times on the same
156+
// instance. To let users write a familiar `fn main` in a file
157+
// named src/main.rs, we provide this `wstd::http_server` macro, which
158+
// transforms the user's `fn main` into the appropriate `handle`
159+
// function.
160+
//
161+
// However, when the top-level file is named src/main.rs, rustc
162+
// requires there to be a function named `main` somewhere in it. This
163+
// requirement can be disabled using `#![no_main]`, however we can't
164+
// use that automatically because macros can't contain inner
165+
// attributes, and we don't want to require users to add `#![no_main]`
166+
// in their own code.
167+
//
168+
// So, we include a definition of a function named `main` here, which
169+
// isn't intended to ever be called, and exists just to satify the
170+
// requirement for a `main` function.
171+
//
172+
// Users could use `#![no_main]` if they want to. Or, they could name
173+
// their top-level file src/lib.rs and add
174+
// ```toml
175+
// [lib]
176+
// crate-type = ["cdylib"]
177+
// ```
178+
// to their Cargo.toml. With either of these, this "main" function will
179+
// be ignored as dead code.
180+
fn main() {
181+
unreachable!("HTTP server components should be run with `handle` rather than `run`")
182+
}
183+
}
184+
.into()
185+
}

src/http/body.rs

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! HTTP body types
22
3-
use crate::io::{AsyncInputStream, AsyncRead, Cursor, Empty};
3+
use crate::http::fields::header_map_from_wasi;
4+
use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Cursor, Empty};
5+
use crate::runtime::AsyncPollable;
46
use core::fmt;
57
use http::header::{CONTENT_LENGTH, TRANSFER_ENCODING};
68
use wasi::http::types::IncomingBody as WasiIncomingBody;
@@ -116,9 +118,9 @@ impl Body for Empty {
116118
pub struct IncomingBody {
117119
kind: BodyKind,
118120
// IMPORTANT: the order of these fields here matters. `body_stream` must
119-
// be dropped before `_incoming_body`.
121+
// be dropped before `incoming_body`.
120122
body_stream: AsyncInputStream,
121-
_incoming_body: WasiIncomingBody,
123+
incoming_body: WasiIncomingBody,
122124
}
123125

124126
impl IncomingBody {
@@ -130,9 +132,29 @@ impl IncomingBody {
130132
Self {
131133
kind,
132134
body_stream,
133-
_incoming_body: incoming_body,
135+
incoming_body,
134136
}
135137
}
138+
139+
/// Consume this `IncomingBody` and return the trailers, if present.
140+
pub async fn finish(self) -> Result<Option<HeaderMap>, Error> {
141+
// The stream is a child resource of the `IncomingBody`, so ensure that
142+
// it's dropped first.
143+
drop(self.body_stream);
144+
145+
let trailers = WasiIncomingBody::finish(self.incoming_body);
146+
147+
AsyncPollable::new(trailers.subscribe()).wait_for().await;
148+
149+
let trailers = trailers.get().unwrap().unwrap()?;
150+
151+
let trailers = match trailers {
152+
None => None,
153+
Some(trailers) => Some(header_map_from_wasi(trailers)?),
154+
};
155+
156+
Ok(trailers)
157+
}
136158
}
137159

138160
impl AsyncRead for IncomingBody {
@@ -177,3 +199,79 @@ impl From<InvalidContentLength> for Error {
177199
ErrorVariant::Other(e.to_string()).into()
178200
}
179201
}
202+
203+
/// The output stream for the body, implementing [`AsyncWrite`]. Call
204+
/// [`Responder::start_response`] to obtain one. Once the body is complete,
205+
/// it must be declared finished, using [`OutgoingBody::finish`].
206+
#[must_use]
207+
pub struct OutgoingBody {
208+
// IMPORTANT: the order of these fields here matters. `stream` must
209+
// be dropped before `body`.
210+
stream: AsyncOutputStream,
211+
body: wasi::http::types::OutgoingBody,
212+
dontdrop: DontDropOutgoingBody,
213+
}
214+
215+
impl OutgoingBody {
216+
pub(crate) fn new(stream: AsyncOutputStream, body: wasi::http::types::OutgoingBody) -> Self {
217+
Self {
218+
stream,
219+
body,
220+
dontdrop: DontDropOutgoingBody,
221+
}
222+
}
223+
224+
pub(crate) fn consume(self) -> (AsyncOutputStream, wasi::http::types::OutgoingBody) {
225+
let Self {
226+
stream,
227+
body,
228+
dontdrop,
229+
} = self;
230+
231+
std::mem::forget(dontdrop);
232+
233+
(stream, body)
234+
}
235+
236+
/// Return a reference to the underlying `AsyncOutputStream`.
237+
///
238+
/// This usually isn't needed, as `OutgoingBody` implements `AsyncWrite`
239+
/// too, however it is useful for code that expects to work with
240+
/// `AsyncOutputStream` specifically.
241+
pub fn stream(&mut self) -> &mut AsyncOutputStream {
242+
&mut self.stream
243+
}
244+
}
245+
246+
impl AsyncWrite for OutgoingBody {
247+
async fn write(&mut self, buf: &[u8]) -> crate::io::Result<usize> {
248+
self.stream.write(buf).await
249+
}
250+
251+
async fn flush(&mut self) -> crate::io::Result<()> {
252+
self.stream.flush().await
253+
}
254+
255+
fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> {
256+
Some(&self.stream)
257+
}
258+
}
259+
260+
/// A utility to ensure that `OutgoingBody` is either finished or failed, and
261+
/// not implicitly dropped.
262+
struct DontDropOutgoingBody;
263+
264+
impl Drop for DontDropOutgoingBody {
265+
fn drop(&mut self) {
266+
unreachable!("`OutgoingBody::drop` called; `OutgoingBody`s should be consumed with `finish` or `fail`.");
267+
}
268+
}
269+
270+
/// A placeholder for use as the type parameter to [`Response`] to indicate
271+
/// that the body has not yet started. This is used with
272+
/// [`Responder::start_response`], which has a `Response<BodyForthcoming>`
273+
/// argument.
274+
///
275+
/// To instead start the response and obtain the output stream for the body,
276+
/// use [`Responder::respond`].
277+
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/error.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::http::fields::ToWasiHeaderError;
12
use std::fmt;
23

34
/// The `http` result type.
@@ -78,9 +79,12 @@ impl From<WasiHttpErrorCode> for Error {
7879
}
7980
}
8081

81-
impl From<WasiHttpHeaderError> for Error {
82-
fn from(e: WasiHttpHeaderError) -> Error {
83-
ErrorVariant::WasiHeader(e).into()
82+
impl From<ToWasiHeaderError> for Error {
83+
fn from(error: ToWasiHeaderError) -> Error {
84+
Error {
85+
variant: ErrorVariant::WasiHeader(error.error),
86+
context: vec![error.context],
87+
}
8488
}
8589
}
8690

0 commit comments

Comments
 (0)