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

Commit e510b3f

Browse files
authored
Merge pull request #58 from bytecodealliance/feat/http
feat: implement `wasi:http`
2 parents 90f0c48 + 5e8f880 commit e510b3f

File tree

88 files changed

+7629
-3076
lines changed

Some content is hidden

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

88 files changed

+7629
-3076
lines changed

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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ tokio = { version = "1.43.0", features = [ "rt", "time" ] }
366366
hyper = "1.0.1"
367367
http = "1.0.0"
368368
http-body = "1.0.0"
369-
http-body-util = "0.1.0"
369+
http-body-util = "0.1.1"
370370
bytes = { version = "1.4", default-features = false }
371371
futures = { version = "0.3.27", default-features = false }
372372
indexmap = { version = "2.0.0", default-features = false }
@@ -476,7 +476,7 @@ disable-logging = ["log/max_level_off", "tracing/max_level_off"]
476476
wasi-nn = ["dep:wasmtime-wasi-nn"]
477477
wasi-tls = ["dep:wasmtime-wasi-tls"]
478478
wasi-threads = ["dep:wasmtime-wasi-threads", "threads"]
479-
wasi-http = ["component-model", "dep:wasmtime-wasi-http", "dep:tokio", "dep:hyper"]
479+
wasi-http = ["component-model", "dep:wasmtime-wasi-http", "wasmtime-wasi-http/p3", "dep:tokio", "dep:hyper"]
480480
wasi-config = ["dep:wasmtime-wasi-config"]
481481
wasi-keyvalue = ["dep:wasmtime-wasi-keyvalue"]
482482
pooling-allocator = ["wasmtime/pooling-allocator", "wasmtime-cli-flags/pooling-allocator"]

ci/vendor-wit.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ make_vendor "wasi/src/p3" "
7575
7676
7777
"
78+
# `wasi:http` from https://github.com/WebAssembly/wasi-http/pull/158
79+
make_vendor "wasi-http/src/p3" "
80+
81+
82+
filesystem@[email protected]
83+
84+
85+
86+
"
7887

7988
rm -rf $cache_dir
8089

crates/test-programs/artifacts/build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@ fn build_and_generate_tests() {
7474
s if s.starts_with("filesystem_0_3") => "filesystem_0_3",
7575
s if s.starts_with("random_0_3") => "random_0_3",
7676
s if s.starts_with("sockets_0_3") => "sockets_0_3",
77+
s if s.starts_with("http_0_3") => "http_0_3",
7778
s if s.starts_with("http_") => "http",
7879
s if s.starts_with("preview1_") => "preview1",
7980
s if s.starts_with("preview2_") => "preview2",
8081
s if s.starts_with("cli_") => "cli",
82+
s if s.starts_with("api_0_3") => "api_0_3",
8183
s if s.starts_with("api_") => "api",
8284
s if s.starts_with("nn_") => "nn",
8385
s if s.starts_with("piped_") => "piped",
@@ -113,7 +115,9 @@ fn build_and_generate_tests() {
113115
s if s.starts_with("filesystem_0_3") => &reactor_adapter,
114116
s if s.starts_with("random_0_3") => &reactor_adapter,
115117
s if s.starts_with("sockets_0_3") => &reactor_adapter,
118+
s if s.starts_with("http_0_3") => &reactor_adapter,
116119
s if s.starts_with("async_") => &reactor_adapter,
120+
s if s.starts_with("api_0_3_proxy") => &proxy_adapter,
117121
s if s.starts_with("api_proxy") => &proxy_adapter,
118122
_ => &command_adapter,
119123
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use futures::join;
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+
let hdrs = Headers::new();
36+
let (mut contents_tx, contents_rx) = wit_stream::new();
37+
let (trailers_tx, trailers_rx) = wit_future::new();
38+
let (resp, transmit) = Response::new(hdrs, Some(contents_rx), trailers_rx);
39+
spawn(async {
40+
join!(
41+
async {
42+
let remaining = contents_tx.write_all(b"hello, world!".to_vec()).await;
43+
assert!(remaining.is_empty());
44+
drop(contents_tx);
45+
trailers_tx
46+
.write(Ok(None))
47+
.await
48+
.expect("failed to write trailers");
49+
},
50+
async {
51+
transmit
52+
.await
53+
.expect("failed to transmit response")
54+
.unwrap()
55+
}
56+
);
57+
});
58+
Ok(resp)
59+
}
60+
}
61+
62+
// Technically this should not be here for a proxy, but given the current
63+
// framework for tests it's required since this file is built as a `bin`
64+
fn main() {}
65+
66+
// TODO: adapt below
67+
//fn test_filesystem() {
68+
// assert!(std::fs::File::open(".").is_err());
69+
//}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use anyhow::Context as _;
2+
use futures::join;
3+
use test_programs::p3::wasi::http::handler;
4+
use test_programs::p3::wasi::http::types::{ErrorCode, Headers, Method, Request, Scheme, Trailers};
5+
use test_programs::p3::{wit_future, wit_stream};
6+
use wit_bindgen::FutureReader;
7+
use wit_bindgen_rt::async_support::{FutureWriter, StreamWriter};
8+
9+
struct Component;
10+
11+
test_programs::p3::export!(Component);
12+
13+
fn make_request() -> (
14+
Request,
15+
StreamWriter<u8>,
16+
FutureWriter<Result<Option<Trailers>, ErrorCode>>,
17+
FutureReader<Result<(), ErrorCode>>,
18+
) {
19+
let (contents_tx, contents_rx) = wit_stream::new();
20+
let (trailers_tx, trailers_rx) = wit_future::new();
21+
let (request, transmit) = Request::new(
22+
Headers::from_list(&[("Content-Length".to_string(), b"11".to_vec())]).unwrap(),
23+
Some(contents_rx),
24+
trailers_rx,
25+
None,
26+
);
27+
28+
request.set_method(&Method::Post).expect("setting method");
29+
request
30+
.set_scheme(Some(&Scheme::Http))
31+
.expect("setting scheme");
32+
let addr = test_programs::p3::wasi::cli::environment::get_environment()
33+
.into_iter()
34+
.find_map(|(k, v)| k.eq("HTTP_SERVER").then_some(v))
35+
.unwrap();
36+
request
37+
.set_authority(Some(&addr))
38+
.expect("setting authority");
39+
request
40+
.set_path_with_query(Some("/"))
41+
.expect("setting path with query");
42+
43+
(request, contents_tx, trailers_tx, transmit)
44+
}
45+
46+
impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
47+
async fn run() -> Result<(), ()> {
48+
{
49+
let (request, mut contents_tx, trailers_tx, transmit) = make_request();
50+
let (transmit, handle) = join!(async { transmit.await }, async {
51+
let res = handler::handle(request)
52+
.await
53+
.context("failed to send request")?;
54+
println!("writing enough");
55+
let remaining = contents_tx.write_all(b"long enough".to_vec()).await;
56+
assert!(
57+
remaining.is_empty(),
58+
"{}",
59+
String::from_utf8_lossy(&remaining)
60+
);
61+
drop(contents_tx);
62+
trailers_tx
63+
.write(Ok(None))
64+
.await
65+
.context("failed to finish body")?;
66+
anyhow::Ok(res)
67+
});
68+
let res = handle.unwrap();
69+
drop(res);
70+
transmit
71+
.expect("transmit sender dropped")
72+
.expect("failed to transmit request");
73+
}
74+
75+
{
76+
let (request, mut contents_tx, trailers_tx, transmit) = make_request();
77+
let (transmit, handle) = join!(async { transmit.await }, async {
78+
let res = handler::handle(request)
79+
.await
80+
.context("failed to send request")?;
81+
println!("writing too little");
82+
let remaining = contents_tx.write_all(b"msg".to_vec()).await;
83+
assert!(
84+
remaining.is_empty(),
85+
"{}",
86+
String::from_utf8_lossy(&remaining)
87+
);
88+
drop(contents_tx);
89+
trailers_tx
90+
.write(Ok(None))
91+
.await
92+
.context("failed to finish body")?;
93+
anyhow::Ok(res)
94+
});
95+
let res = handle.unwrap();
96+
drop(res);
97+
let err = transmit
98+
.expect("transmit sender dropped")
99+
.expect_err("request transmission should have failed");
100+
assert!(
101+
matches!(err, ErrorCode::HttpRequestBodySize(Some(3))),
102+
"unexpected error: {err:#?}"
103+
);
104+
}
105+
106+
{
107+
let (request, mut contents_tx, trailers_tx, transmit) = make_request();
108+
let (transmit, handle) = join!(async { transmit.await }, async {
109+
let res = handler::handle(request)
110+
.await
111+
.context("failed to send request")?;
112+
println!("writing too much");
113+
let remaining = contents_tx.write_all(b"more than 11 bytes".to_vec()).await;
114+
assert!(
115+
remaining.is_empty(),
116+
"{}",
117+
String::from_utf8_lossy(&remaining)
118+
);
119+
drop(contents_tx);
120+
_ = trailers_tx.write(Ok(None)).await;
121+
anyhow::Ok(res)
122+
});
123+
let res = handle.unwrap();
124+
drop(res);
125+
let err = transmit
126+
.expect("transmit sender dropped")
127+
.expect_err("request transmission should have failed");
128+
assert!(
129+
matches!(err, ErrorCode::HttpRequestBodySize(Some(18))),
130+
"unexpected error: {err:#?}"
131+
);
132+
}
133+
Ok(())
134+
}
135+
}
136+
137+
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() {}

0 commit comments

Comments
 (0)