Skip to content

Commit 196b004

Browse files
authored
Merge pull request #25 from jasisz/0.19-http-wasip2
http on --wasip2 (all 6 methods + headers)
2 parents f7f6d61 + 14268cf commit 196b004

17 files changed

Lines changed: 3849 additions & 22 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes to Aver are documented here. Starting with 0.10.0, minor releases get a codename — short, evocative, and it tells you what the release was really about.
44

5+
## 0.19.0 (unreleased)
6+
7+
### Added
8+
- **HTTP client on `--target wasip2`.** All six methods — `Http.get`, `Http.head`, `Http.delete`, `Http.post`, `Http.put`, `Http.patch` — now compile and run as components. Response headers surface as `Map<String, List<String>>`; multi-value headers (e.g. `Set-Cookie`) keep server emit order. Failure messages name the wasi:http error variant (`http: connection-refused`, `http: DNS-timeout`, …) instead of a generic string.
9+
510
## 0.18.0 "Span" (2026-05-09)
611

712
> _Cross the Component Model boundary the same way Aver crosses the source/wasm one — typed effects in, canonical-ABI imports out._

Cargo.lock

Lines changed: 135 additions & 0 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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ wasm-compile = ["dep:wasm-encoder", "dep:wasmprinter", "dep:wasmparser", "dep:wa
5454
# to WIT/WASI imports — there is no preview1 ABI to preserve. See
5555
# docs/wasip2.md for the contract and feedback_aver_no_preview1_adapter
5656
# for the architectural rationale.
57-
wasip2 = ["wasm-compile", "dep:wit-component", "dep:wit-parser", "dep:wasmtime", "dep:wasmtime-wasi"]
57+
wasip2 = ["wasm-compile", "dep:wit-component", "dep:wit-parser", "dep:wasmtime", "dep:wasmtime-wasi", "dep:wasmtime-wasi-http"]
5858
playground = ["wasm-compile", "dep:wasm-bindgen", "runtime", "tty-render"]
5959

6060
[dependencies]
@@ -74,6 +74,7 @@ wasmparser = { version = "0.248", optional = true }
7474
wasmprinter = { version = "0.248", optional = true }
7575
wasmtime = { version = "44", optional = true, default-features = false, features = ["cranelift", "runtime", "gc", "gc-drc", "component-model", "component-model-async"] }
7676
wasmtime-wasi = { version = "44", optional = true }
77+
wasmtime-wasi-http = { version = "44", optional = true }
7778
wat = { version = "1", optional = true }
7879
# wasip2 feature deps — versions paired by upstream wasm-tools 1.248
7980
# release train. `wasi-preview1-component-adapter-provider` is NOT

docs/wasip2.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,9 @@ Aver effects lower directly to WASI 0.2 imports. The mapping is fixed per effect
102102
| `Time.now` / `unixMs` | `wasi:clocks/wall-clock.now` (Time.now formats RFC3339 guest-side via Howard Hinnant's `civil_from_days`) |
103103
| `Time.sleep` | `wasi:clocks/monotonic-clock.subscribe-duration` + `wasi:io/poll.poll` + `[resource-drop]pollable` (per-call pollable, real wait — not busy-loop) |
104104
| `Random.int` / `float` | `wasi:random/random.get-random-u64` + Aver-side range scaling. This is the secure `wasi:random/random` interface (same contract as `get-random-bytes`, just returning 8 cryptographically-secure bytes packed into a u64); we deliberately do NOT use `wasi:random/insecure.get-insecure-random-u64`. If we later need finer byte-level control (e.g. for `Random.bytes(n)`), the switch to `get-random-bytes` is mechanical. |
105-
| `Http.*` | **Compile-rejected** — out of 0.18 scope (Phase 2 / 0.19) |
106-
| `HttpServer.listen` / `listenWith` | **Compile-rejected** — out of 0.18 scope (Phase 3 / 0.19) |
107-
| `Tcp.*` | **Compile-rejected** — out of 0.18 scope (Phase 2 / 0.19) |
105+
| `Http.{get, head, delete, post, put, patch}` | `wasi:http/outgoing-handler.handle` + the future-incoming-response / incoming-response choreography (Phase 2 / 0.19 shipped). Method tag selects `outgoing-request.set-method`. Body-bearing verbs marshal a request body via `request.body` + `outgoing-body.write` + chunked `blocking-write-and-flush` + `outgoing-body.finish`. Headers (request and response) lower as `Map<String, List<String>>`; multi-valued field names preserve server emit order. `error-code` variant discriminants surface as per-variant `http: <name>` Err messages (39 cases). |
106+
| `HttpServer.listen` / `listenWith` | **Compile-rejected** — out of 0.19 client scope (Phase 3 / 0.19+); requires the `wasi:http/proxy` world + exported `incoming-handler.handle`. |
107+
| `Tcp.*` | **Compile-rejected** — out of 0.19 client scope (Phase 2.1 / 0.19+) |
108108
| `Terminal.*` (12 methods) | **Compile-rejected** — WASI 0.2 has no raw/cooked-mode operations |
109109

110110
### Why `Terminal.*` / `Env.set` are rejected, not stubbed

src/codegen/wasip2/effect_check.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,15 @@ fn classify(effect: &str) -> Option<UnsupportedReason> {
137137
// inside `__rt_time_sleep`. The pollable model is hidden in the
138138
// helper; source-level Aver still sees `Time.sleep(ms) -> Unit`.
139139
// ---------- Out of 0.18 release scope ----------
140-
if effect.starts_with("Http.") {
141-
return Some(UnsupportedReason::OutOfRelease {
142-
phase: "Phase 2 / 0.19",
143-
});
140+
// All wasi:http verbs graduated in 0.19: GET in Phase 2.0,
141+
// HEAD/DELETE in Phase 2.J (share pipeline via set-method),
142+
// POST/PUT/PATCH in Phase 2.K (outgoing-body marshalling +
143+
// user headers iteration + Content-Type).
144+
if matches!(
145+
effect,
146+
"Http.get" | "Http.head" | "Http.delete" | "Http.post" | "Http.put" | "Http.patch"
147+
) {
148+
return None;
144149
}
145150
if effect.starts_with("Tcp.") {
146151
return Some(UnsupportedReason::OutOfRelease {
@@ -217,10 +222,8 @@ mod tests {
217222

218223
#[test]
219224
fn classifies_out_of_release_rejects() {
220-
assert!(matches!(
221-
classify("Http.get"),
222-
Some(UnsupportedReason::OutOfRelease { .. })
223-
));
225+
// All Http.* graduated in 0.19; only Tcp / HttpServer
226+
// remain rejected as out-of-release.
224227
assert!(matches!(
225228
classify("Tcp.connect"),
226229
Some(UnsupportedReason::OutOfRelease { .. })
@@ -231,6 +234,17 @@ mod tests {
231234
));
232235
}
233236

237+
#[test]
238+
fn all_http_methods_graduate() {
239+
// Phase 2.0 (GET) + 2.J (HEAD/DELETE) + 2.K (POST/PUT/PATCH).
240+
assert!(classify("Http.get").is_none());
241+
assert!(classify("Http.head").is_none());
242+
assert!(classify("Http.delete").is_none());
243+
assert!(classify("Http.post").is_none());
244+
assert!(classify("Http.put").is_none());
245+
assert!(classify("Http.patch").is_none());
246+
}
247+
234248
#[test]
235249
fn classifies_pending_phase_rejects() {
236250
// Console.print/error/warn graduated in 1.2b1.5; Time.unixMs

src/codegen/wasip2/wit.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use super::wrap::Wasip2World;
2828
/// end-to-end on the empty world (component is a valid WASI 0.2
2929
/// artifact, just with no public surface). Phase 1.2 turns this
3030
/// into a meaningful world declaration.
31-
pub fn emit_world_wit(world: Wasip2World) -> String {
31+
pub fn emit_world_wit(world: Wasip2World, needs_http: bool) -> String {
3232
let local_name = world.local_name();
3333
let (header_note, body) = match world {
3434
// `wasi:cli/command` is the canonical CLI shape from the
@@ -38,15 +38,32 @@ pub fn emit_world_wit(world: Wasip2World) -> String {
3838
// The wasm-gc emitter exports `wasi:cli/run@0.2.4#run`
3939
// (canonical-ABI mangled name) when `target == Wasip2`, so
4040
// the contract is satisfiable at this commit.
41-
Wasip2World::CliCommand => (
42-
"// Phase 1.2b1.3 — wasm-gc emits `wasi:cli/run@0.2.4#run` to satisfy this world.",
43-
" include wasi:cli/command@0.2.4;",
44-
),
41+
//
42+
// `wasi:cli/command` does not transitively include
43+
// `wasi:http/*`. When the user program reaches an `Http.*`
44+
// effect, the wasm-gc backend emits canonical-ABI imports
45+
// of `wasi:http/outgoing-handler@0.2.4` (and types via the
46+
// implicit transitive package); the world has to declare
47+
// that import explicitly so `wit-component::ComponentEncoder`
48+
// matches it against the host. `needs_http` is derived in
49+
// `wrap.rs` by scanning the core module's imports — single
50+
// source of truth, no risk of drift between codegen and
51+
// world declaration.
52+
Wasip2World::CliCommand => {
53+
let header =
54+
"// Phase 1.2b1.3 — wasm-gc emits `wasi:cli/run@0.2.4#run` to satisfy this world.";
55+
let body = if needs_http {
56+
" include wasi:cli/command@0.2.4;\n \n // Phase 2 / 0.19 — Http.* effects on `--target wasip2` lower\n // directly to `wasi:http/outgoing-handler.handle` plus the\n // future-incoming-response / incoming-response choreography.\n // WASI 0.3 collapses this into native `future<T>` / `stream<u8>`\n // types and three imports; a `Wasip3World` variant lands when\n // the spec stabilises.\n import wasi:http/outgoing-handler@0.2.4;".to_string()
57+
} else {
58+
" include wasi:cli/command@0.2.4;".to_string()
59+
};
60+
(header, body)
61+
}
4562
// Phase 3 / 0.19. Compile-rejected upstream in `wrap.rs`; the
4663
// body still pretty-prints something honest for the artifact.
4764
Wasip2World::HttpProxy => (
4865
"// Phase 3 / 0.19 — compile-rejected.",
49-
" include wasi:http/proxy@0.2.4;",
66+
" include wasi:http/proxy@0.2.4;".to_string(),
5067
),
5168
};
5269
format!(

src/codegen/wasip2/wrap.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,17 @@ pub fn compile_to_component(
8787
let mut resolve = Resolve::default();
8888
super::wasi_bundle::push_wasi_packages(&mut resolve)?;
8989

90-
let wit_source = super::wit::emit_world_wit(world);
90+
// Inspect the core module's imports to decide whether the world
91+
// needs `import wasi:http/outgoing-handler@0.2.4`. We scan
92+
// because the codegen path that registers wasi:http slots
93+
// (`module.rs` `EffectName::HttpGet` arm) and the wrap path live
94+
// in different modules — having `compile_to_component` derive the
95+
// bit from the only ground truth (the actual emitted imports)
96+
// keeps the world exactly tracking the core's surface and avoids
97+
// a second source of truth that could drift.
98+
let needs_http = core_imports_use_wasi_http(core_wasm);
99+
100+
let wit_source = super::wit::emit_world_wit(world, needs_http);
91101

92102
// Parse our generated WIT into the same `Resolve`. `parse` reads
93103
// from a string — the path argument is for error messages only,
@@ -123,3 +133,29 @@ pub fn compile_to_component(
123133

124134
Ok((component, wit_source))
125135
}
136+
137+
/// Scan a core wasm module's imports for any module name starting
138+
/// with `"wasi:http/"`. Used by `compile_to_component` to decide
139+
/// whether the generated WIT world should additionally
140+
/// `import wasi:http/outgoing-handler@0.2.4`.
141+
///
142+
/// Implemented via `wasmparser` to walk only the import section —
143+
/// avoids re-decoding the full module the way `Module::new` would,
144+
/// and treats malformed bytes as "no http imports" (the encoder
145+
/// later rejects malformed input with a clearer message).
146+
fn core_imports_use_wasi_http(core_wasm: &[u8]) -> bool {
147+
use wasmparser::{Parser, Payload};
148+
for payload in Parser::new(0).parse_all(core_wasm).flatten() {
149+
if let Payload::ImportSection(reader) = payload {
150+
// `into_imports()` flattens the grouped `Imports` enum
151+
// (Single / Compact1 / Compact2) into a per-item iterator
152+
// of `Import` values, each with its own `module` field.
153+
for import in reader.into_imports().flatten() {
154+
if import.module.starts_with("wasi:http/") {
155+
return true;
156+
}
157+
}
158+
}
159+
}
160+
false
161+
}

0 commit comments

Comments
 (0)