Skip to content
This repository was archived by the owner on Sep 8, 2025. It is now read-only.

Commit b4e85a9

Browse files
committed
feat(p3): implement wasi:http
Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net>
1 parent 80a85e6 commit b4e85a9

86 files changed

Lines changed: 7028 additions & 2945 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 10 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ tokio = { version = "1.43.0", features = [ "rt", "time" ] }
364364
hyper = "1.0.1"
365365
http = "1.0.0"
366366
http-body = "1.0.0"
367-
http-body-util = "0.1.0"
367+
http-body-util = "0.1.1"
368368
bytes = { version = "1.4", default-features = false }
369369
futures = { version = "0.3.27", default-features = false }
370370
indexmap = { version = "2.0.0", default-features = false }

ci/vendor-wit.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ make_vendor "wasi/src/p3" "
7070
random@9499404@wit-0.3.0-draft
7171
sockets@41d7079@wit-0.3.0-draft
7272
"
73+
# `wasi:http` from https://github.com/WebAssembly/wasi-http/pull/158
74+
make_vendor "wasi-http/src/p3" "
75+
cli@82b86d9@wit-0.3.0-draft
76+
clocks@646092f@wit-0.3.0-draft
77+
filesystem@740cd76@wit-0.3.0-draft
78+
random@9499404@wit-0.3.0-draft
79+
sockets@41d7079@wit-0.3.0-draft
80+
http@ae89575@wit-0.3.0-draft
81+
"
7382

7483
rm -rf $cache_dir
7584

crates/test-programs/artifacts/build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,12 @@ fn build_and_generate_tests() {
7373
s if s.starts_with("filesystem_0_3") => "filesystem_0_3",
7474
s if s.starts_with("random_0_3") => "random_0_3",
7575
s if s.starts_with("sockets_0_3") => "sockets_0_3",
76+
s if s.starts_with("http_0_3") => "http_0_3",
7677
s if s.starts_with("http_") => "http",
7778
s if s.starts_with("preview1_") => "preview1",
7879
s if s.starts_with("preview2_") => "preview2",
7980
s if s.starts_with("cli_") => "cli",
81+
s if s.starts_with("api_0_3") => "api_0_3",
8082
s if s.starts_with("api_") => "api",
8183
s if s.starts_with("nn_") => "nn",
8284
s if s.starts_with("piped_") => "piped",
@@ -111,7 +113,9 @@ fn build_and_generate_tests() {
111113
s if s.starts_with("filesystem_0_3") => &reactor_adapter,
112114
s if s.starts_with("random_0_3") => &reactor_adapter,
113115
s if s.starts_with("sockets_0_3") => &reactor_adapter,
116+
s if s.starts_with("http_0_3") => &reactor_adapter,
114117
s if s.starts_with("async_") => &reactor_adapter,
118+
s if s.starts_with("api_0_3_proxy") => &proxy_adapter,
115119
s if s.starts_with("api_proxy") => &proxy_adapter,
116120
_ => &command_adapter,
117121
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use futures::{join, SinkExt as _};
2+
use test_programs::p3::wasi::http::types::{ErrorCode, Headers, Request, Response};
3+
use test_programs::p3::{wit_future, wit_stream};
4+
use wit_bindgen_rt::async_support::spawn;
5+
6+
struct T;
7+
8+
test_programs::p3::proxy::export!(T);
9+
10+
impl test_programs::p3::proxy::exports::wasi::http::handler::Guest for T {
11+
async fn handle(request: Request) -> Result<Response, ErrorCode> {
12+
assert!(request.scheme().is_some());
13+
assert!(request.authority().is_some());
14+
assert!(request.path_with_query().is_some());
15+
16+
// TODO: adapt below
17+
//test_filesystem();
18+
19+
let header = String::from("custom-forbidden-header");
20+
let req_hdrs = request.headers();
21+
22+
assert!(
23+
!req_hdrs.has(&header),
24+
"forbidden `custom-forbidden-header` found in request"
25+
);
26+
27+
assert!(req_hdrs.delete(&header).is_err());
28+
assert!(req_hdrs.append(&header, b"no".as_ref()).is_err());
29+
30+
assert!(
31+
!req_hdrs.has(&header),
32+
"append of forbidden header succeeded"
33+
);
34+
35+
assert!(
36+
!req_hdrs.has("host"),
37+
"forbidden host header present in incoming request"
38+
);
39+
40+
let hdrs = Headers::new();
41+
let (mut contents_tx, contents_rx) = wit_stream::new();
42+
let (trailers_tx, trailers_rx) = wit_future::new();
43+
let (resp, transmit) = Response::new(hdrs, Some(contents_rx), trailers_rx);
44+
spawn(async {
45+
join!(
46+
async {
47+
contents_tx
48+
.send(b"hello, world!".to_vec())
49+
.await
50+
.expect("writing response");
51+
drop(contents_tx);
52+
trailers_tx.write(Ok(None));
53+
},
54+
async {
55+
transmit
56+
.await
57+
.expect("failed to transmit response")
58+
.unwrap()
59+
.unwrap()
60+
}
61+
);
62+
});
63+
Ok(resp)
64+
}
65+
}
66+
67+
// Technically this should not be here for a proxy, but given the current
68+
// framework for tests it's required since this file is built as a `bin`
69+
fn main() {}
70+
71+
// TODO: adapt below
72+
//fn test_filesystem() {
73+
// assert!(std::fs::File::open(".").is_err());
74+
//}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use futures::SinkExt as _;
2+
use test_programs::p3::wasi::http::types::{ErrorCode, Headers, Method, Request, Scheme, Trailers};
3+
use test_programs::p3::{wit_future, wit_stream};
4+
use wit_bindgen_rt::async_support::{FutureWriter, StreamWriter};
5+
6+
struct Component;
7+
8+
test_programs::p3::export!(Component);
9+
10+
fn make_request() -> (
11+
Request,
12+
StreamWriter<u8>,
13+
FutureWriter<Result<Option<Trailers>, ErrorCode>>,
14+
) {
15+
let (contents_tx, contents_rx) = wit_stream::new();
16+
let (trailers_tx, trailers_rx) = wit_future::new();
17+
let (request, _) = Request::new(
18+
Headers::from_list(&[("Content-Length".to_string(), b"11".to_vec())]).unwrap(),
19+
Some(contents_rx),
20+
trailers_rx,
21+
None,
22+
);
23+
24+
request.set_method(&Method::Post).expect("setting method");
25+
request
26+
.set_scheme(Some(&Scheme::Http))
27+
.expect("setting scheme");
28+
let addr = test_programs::p3::wasi::cli::environment::get_environment()
29+
.into_iter()
30+
.find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v))
31+
.unwrap();
32+
request
33+
.set_authority(Some(&addr))
34+
.expect("setting authority");
35+
request
36+
.set_path_with_query(Some("/"))
37+
.expect("setting path with query");
38+
39+
(request, contents_tx, trailers_tx)
40+
}
41+
42+
impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
43+
async fn run() -> Result<(), ()> {
44+
{
45+
println!("writing enough");
46+
let (_, mut contents_tx, trailers_tx) = make_request();
47+
contents_tx.send(b"long enough".to_vec()).await.unwrap();
48+
drop(contents_tx);
49+
trailers_tx.write(Ok(None)).await;
50+
}
51+
52+
{
53+
println!("writing too little");
54+
let (_, mut contents_tx, trailers_tx) = make_request();
55+
contents_tx.send(b"msg".to_vec()).await.unwrap();
56+
drop(contents_tx);
57+
trailers_tx.write(Ok(None)).await;
58+
59+
// handle()
60+
61+
// TODO: Figure out how/if to represent this in wasip3
62+
//let e = OutgoingBody::finish(outgoing_body, None)
63+
// .expect_err("finish should fail");
64+
65+
//assert!(
66+
// matches!(&e, ErrorCode::HttpRequestBodySize(Some(3))),
67+
// "unexpected error: {e:#?}"
68+
//);
69+
}
70+
71+
{
72+
println!("writing too much");
73+
let (_, mut contents_tx, trailers_tx) = make_request();
74+
contents_tx
75+
.send(b"more than 11 bytes".to_vec())
76+
.await
77+
.unwrap();
78+
drop(contents_tx);
79+
trailers_tx.write(Ok(None)).await;
80+
81+
// TODO: Figure out how/if to represent this in wasip3
82+
//let e = request_body
83+
// .blocking_write_and_flush("more than 11 bytes".as_bytes())
84+
// .expect_err("write should fail");
85+
//let e = match e {
86+
// test_programs::wasi::io::streams::StreamError::LastOperationFailed(e) => {
87+
// http_error_code(&e)
88+
// }
89+
// test_programs::wasi::io::streams::StreamError::Closed => panic!("request closed"),
90+
//};
91+
//assert!(
92+
// matches!(
93+
// e,
94+
// Some(ErrorCode::HttpRequestBodySize(Some(18)))
95+
// ),
96+
// "unexpected error {e:?}"
97+
//);
98+
//let e = OutgoingBody::finish(outgoing_body, None)
99+
// .expect_err("finish should fail");
100+
101+
//assert!(
102+
// matches!(&e, ErrorCode::HttpRequestBodySize(Some(18))),
103+
// "unexpected error: {e:#?}"
104+
//);
105+
}
106+
Ok(())
107+
}
108+
}
109+
110+
fn main() {}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use anyhow::Context;
2+
use test_programs::p3::wasi::http::types::{Method, Scheme};
3+
4+
struct Component;
5+
6+
test_programs::p3::export!(Component);
7+
8+
impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
9+
async fn run() -> Result<(), ()> {
10+
let addr = test_programs::p3::wasi::cli::environment::get_environment()
11+
.into_iter()
12+
.find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v))
13+
.unwrap();
14+
let res = test_programs::p3::http::request(
15+
Method::Get,
16+
Scheme::Http,
17+
&addr,
18+
"/get?some=arg&goes=here",
19+
None,
20+
None,
21+
None,
22+
None,
23+
None,
24+
)
25+
.await
26+
.context("/get")
27+
.unwrap();
28+
29+
println!("{addr} /get: {res:?}");
30+
assert_eq!(res.status, 200);
31+
let method = res.header("x-wasmtime-test-method").unwrap();
32+
assert_eq!(std::str::from_utf8(method).unwrap(), "GET");
33+
let uri = res.header("x-wasmtime-test-uri").unwrap();
34+
assert_eq!(
35+
std::str::from_utf8(uri).unwrap(),
36+
format!("/get?some=arg&goes=here")
37+
);
38+
assert_eq!(res.body, b"");
39+
Ok(())
40+
}
41+
}
42+
43+
fn main() {}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use test_programs::p3::wasi::http::types::{ErrorCode, Method, Scheme};
2+
3+
struct Component;
4+
5+
test_programs::p3::export!(Component);
6+
7+
impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
8+
async fn run() -> Result<(), ()> {
9+
let res = test_programs::p3::http::request(
10+
Method::Get,
11+
Scheme::Http,
12+
"some.invalid.dnsname:3000",
13+
"/",
14+
None,
15+
None,
16+
None,
17+
None,
18+
None,
19+
)
20+
.await;
21+
22+
let e = res.unwrap_err();
23+
assert!(
24+
matches!(
25+
e.downcast_ref::<ErrorCode>()
26+
.expect("expected a wasi-http ErrorCode"),
27+
ErrorCode::DnsError(_) | ErrorCode::ConnectionRefused,
28+
),
29+
"Unexpected error: {e:#?}"
30+
);
31+
Ok(())
32+
}
33+
}
34+
35+
fn main() {}

0 commit comments

Comments
 (0)