http on --wasip2 (all 6 methods + headers)#25
Merged
Merged
Conversation
Add `wasmtime-wasi-http = 44.0.1` as a `wasip2`-feature dep, attach `WasiHttpCtx` + `WasiHttpView` impl to the existing CLI host, and register `wasi:http/*` via `add_only_http_to_linker_sync` (the full `add_to_linker_sync` re-registers wasi:io / wasi:cli / wasi:clocks that `wasmtime_wasi::p2::add_to_linker_sync` already wired and trips "map entry wasi:io/error@0.2.x defined twice"). Components that don't import wasi:http see no behaviour change — the dispatcher is dormant unless the guest calls into it. Setup needed before the wasm-gc backend can lower `Http.get` to `wasi:http/outgoing-handler.handle` in step 2. baseline regressions: - `aver run --wasip2` on a Console-only program still prints - `tests/wasip2_poc.rs` 3/3 ✅ - `tests/wasip2_stress.rs` 6/6 ✅ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
aver | 14268cf | Commit Preview URL | May 12 2026, 07:28 PM |
Declare every canonical-ABI import the upcoming `__rt_http_get` helper will need, with full doc-comments documenting each slot's WIT signature, canonical-ABI lowering shape, retptr layout where applicable, and the WASI 0.2 ceremony each represents: - 12 method/constructor slots: fields/outgoing-request constructors, outgoing-request setters (scheme, authority, path-with-query), outgoing-handler.handle, future-incoming-response.subscribe/get, incoming-response.status/consume, incoming-body.stream/finish. - 4 resource-drop slots: outgoing-request, future-incoming-response, incoming-response, future-trailers. Implementations populate `module_field_pair`, `params`, `results` matching `wit_component::ComponentEncoder`'s name resolution + the canonical-ABI flat-vs-retptr discipline. No callers yet (Step D will add `__rt_http_get`); rustc emits a dead_code warning for the new variants, accepted until callers land. The slot enum already has 27 wasi:cli / wasi:io / wasi:filesystem / wasi:clocks / wasi:random variants from 0.18; adding wasi:http here keeps the registry shape uniform — no parallel http registry, no sub-enum nesting. WASI 0.3 will get a sibling `Wasip3ImportSlot` when the time comes; the same pattern repeats with ~3 slots instead of 12 (native future<T> / stream<u8> collapse the choreography). baseline checks: - `cargo build --features wasip2 --bin aver` ✓ (one expected dead_code warning on the new variants) - `tests/wasip2_poc.rs` 3/3 ✓ - `tests/wasip2_stress.rs` 6/6 ✓ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B (registry tracking, src/codegen/wasm_gc/{effects,module}.rs):
EffectName::HttpGet now returns true from `lowers_on_wasip2`, and
the per-effect dispatch in module.rs adds an arm registering the
12 new HttpTypes/HttpOutgoingHandler slots plus 4 reused ones
(IoPollPoll / IoPollResourceDropPollable / InputStreamBlockingRead
/ IoStreamsResourceDropInputStream — all already wired by Time.sleep
and Disk.readText). 16-slot footprint per Http.get is just how
WASI 0.2 spells "outgoing HTTP request"; the comment in module.rs
points at WASI 0.3 which collapses this to ~3 imports.
The registration is dormant in this commit: `effect_check.rs`
still rejects every `Http.*` upstream, so no real program reaches
the wasm-gc emitter yet. Step E flips effect_check.
C (WIT world, src/codegen/wasip2/{wrap,wit}.rs):
`compile_to_component` now scans the core wasm's import section
and detects whether any module name starts with `wasi:http/`.
That bit threads through `emit_world_wit(world, needs_http)` which
appends `import wasi:http/outgoing-handler@0.2.4;` to the
`wasi:cli/command`-include line when needed. Source of truth is
the actual core imports — no second flag to keep in sync between
codegen and wrap.
Empty programs and non-Http programs emit identical WIT to before
(needs_http=false → unchanged body). Future Wasip3World variant
will branch on the same shape, just with a different include and
fewer imports per effect.
baseline checks:
- `cargo build --features wasip2 --bin aver` ✓
- `tests/wasip2_poc.rs` 3/3 ✓
- `tests/wasip2_stress.rs` 6/6 ✓
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`emit_http_get` lowers the full wasi:http/outgoing-handler.handle pipeline to a single wasm-encoder Function: URL parse (scheme + authority + path-with-query), fields constructor, outgoing-request constructor + setters, handle (with Err short-circuit), pollable subscribe + poll + drop, future.get with three-layer discriminant checks (option-Some / middle-Ok / inner-Ok at offsets 0, 8, 16 respectively — error-code's `option<u64>` payload forces align=8 through every wrapping layer), status, consume, body.stream, drain loop, per-call resource drops, and HttpResponse build wrapped in Result.Ok. v1 PoC scope: plain http://, empty response headers (incoming- response.headers slot not registered yet), minimal URL parser (no userinfo / no IPv6 brackets / no port-only authority). Folds the cargo fmt drift from `a4f23b14` into the same commit so the next CI run clears the Format gate.
…wasip2 Allocates `HttpGetIndices` after `disk_list_dir` in `module.rs` (gated on all 16 new wasi:http slots + 4 reused + String / Result / HttpResponse / Map<String, List<String>> type slots), threads the helper bundle through `funcs.function` + `Wasip2Lowering. http_get_fn_idx` + `codes.function`, and adds `emit_http_get_wasip2` to `body/builtins_wasip2.rs` for the call-site lowering. `effect_check.rs` graduates `Http.get` to None (lowered via direct WIT) and bumps the rest of `Http.*` to "Phase 2.1 / 0.19" so the diagnostic stays helpful for the still-rejected verbs. End-to-end test in `tests/wasip2_http.rs` spins up a Python http. server bound to OS-assigned port (parsed from stdout to avoid fixed- port flakes), compiles + runs an Aver program calling Http.get, asserts status=200 and body length matches the served fixture.
…eaders Step F — eliminate the incoming-body resource leak on error paths between consume() and finish(). Adds [resource-drop]incoming-body slot; wires drop_incoming_body_fn through HttpGetHelperFns; calls it on body.stream Err and blocking-read non-closed Err in child→parent order (body before response). Step G — replace the empty-headers placeholder with the real response.headers map. Adds three slots: - [method]incoming-response.headers (-> own<fields>) - [method]fields.entries (-> list<tuple<field-key, field-value>>) - [resource-drop]fields (child of incoming-response, must drop before drop_incoming_response per WIT spec) Wires from_lm_fn (bridge helper) and map_set_fn (Map<String, List<String>> helper) through HttpGetHelperFns. The body emit loops over the entries list, memory.copy's each (name, value) pair into LM[0..len], lifts via __rt_string_from_lm, builds a singleton List<String> per value, and folds into the headers map via Map.set. Multi-valued headers (same field-key in multiple entries — e.g. Set-Cookie) currently overwrite via Map.set rather than appending to the existing list. Surfacing the full multi-set requires Map.get + List.prepend per entry; deferred since real HTTP responses rarely repeat header names. Test extended to assert headers_len > 0 and content-type lookup returns "text/html" (Python http.server normalises field names to lowercase per HTTP/1.1 case-insensitivity).
Step F+G's first cut overwrote duplicate field-keys via plain Map.set, losing all values past the first for headers like Set-Cookie that legitimately appear multiple times in a single response. RFC 6265 requires preserving server-emit order for Set-Cookie specifically; HTTP/1.1 §3.2.2 allows duplicate field names with order-significant semantics for non-comma-combinable headers. Restructures the entry loop to: - iterate REVERSE (idx = entries_len-1..=0 via signed compare) - per entry, probe `Map.get(map, name)` → Option<List<String>> - build new list = struct.new $list_string (val, tail) where tail = Some(prev) ⇒ prev, None ⇒ ref.null - Map.set(map, name, new_list) Reverse iteration + prepend yields forward order in the final list without needing List.append (which would be O(n²) over the loop). E.g. server emits Set-Cookie: a, then Set-Cookie: b → processing b first builds [b], processing a next builds [a, b]. Wires Option<List<String>> type idx and Map.get fn idx through HttpGetIndices/HttpGetHelperFns; the type was already auto- registered by the Map<K,V> discovery walk. New test http_get_preserves_multi_value_header_order spawns a custom Python BaseHTTPRequestHandler that emits three Set-Cookie + two X-Order headers in known order and asserts the Aver-side list comma-joins to the exact server-emitted sequence. Failure mode regresses to overwrite-wins, missing-list, or reversed order — any of those would change the asserted string.
Replace the generic "http: response error" with a per-discriminant dispatch that surfaces the wasi:http error-code variant name (connection-refused, DNS-timeout, TLS-protocol-error, …) verbatim. v1: variant name only — payload extraction (DNS-error's rcode, TLS-alert-received's alert-message, internal-error's option <string>, etc.) deferred since most ops debugging needs the variant tag, not the payload detail. The host writes the error-code discriminant as a u8 at retptr+24 (error-code is the inner result's Err payload; payload starts at +24, error-code's variant discriminant is the first byte). Dispatch is a flat 39-case if-chain — table-driven `array.new_data` over per-name data segments would shave ~5 KB of wasm code, but requires coordination with module.rs's data-section emit; deferred. New test http_get_surfaces_connection_refused binds to port 0 then closes the listener, calls Http.get against the now-free port, asserts the surfaced Err contains a recognisable connection-refused / connection-failed / connection-terminated message (host TCP backoff sometimes routes through handle()'s retptr Err arm rather than future.get's inner Err — both paths are valid; the assertion accepts either).
Generalises the helper from `__rt_http_get(url)` to `__rt_http_request(method_tag, url)`. method_tag uses wasi:http's `method` variant ordinals (0=GET, 1=HEAD, 4=DELETE; 2=POST, 3=PUT, 8=PATCH land in Step K). Adds [method]outgoing-request.set-method slot. After the request constructor, a single `if method_tag != 0` gates the set-method call, keeping the GET fast path identical to the original helper. Constructor defaults to GET, so calling set-method only when needed avoids an extra host round-trip on the most common verb. Per-method dispatch refactored to a shared `emit_http_method_ wasip2(name, tag, ...)` in body/builtins_wasip2.rs; `Http.get`, `Http.head`, `Http.delete` thin wrappers each push the right ordinal. Slot registration in module.rs broadens to match `HttpGet | HttpHead | HttpDelete`; effect_check.rs graduates all three on wasip2 (rejects post/put/patch with "Phase 2.K / 0.19" hint). New test http_head_and_delete_dispatch_correct_method spawns a custom Python BaseHTTPRequestHandler tagging responses with `X-Method: <verb>`. Aver fires GET/HEAD/DELETE in turn and the test asserts each surfaces the matching method header. HEAD also asserts an empty body (server skips wfile.write per HTTP spec).
Generalises __rt_http_request to take 5 params (method_tag, url, content_type, body, headers map). Body-less methods (GET/HEAD/ DELETE) keep their per-call dispatchers; the new body-bearing dispatchers push user args verbatim. Both helper variants share the same underlying fn — body marshalling gates on `method >= 2 && method != 4` so GET/HEAD/DELETE skip it entirely. Five new wasi:http slots: - [method]outgoing-request.body → result<own<outgoing-body>> - [method]outgoing-body.write → result<own<output-stream>> - [static]outgoing-body.finish → result<_, error-code> - [method]fields.append → result<_, header-error> (via retptr — flat encoding flattens to 2 i32s, exceeds MAX_FLAT_RESULTS) - [resource-drop]outgoing-body (error path between body() and finish()) Reused: OutputStreamBlockingWriteAndFlush + drop, both already wired by Console.print. Bug found mid-implementation: the URL parser leaves auth_ptr / path_ptr pointing into LM[0..url_len], but the to_lm calls for header values + Content-Type + body all clobber LM[0..]. Fix: copy authority + path bytes to the cabi_realloc bump heap right after URL parse, repoint the locals at the bump heap. set- authority / set-path-with-query then read from a stable address that no subsequent to_lm can corrupt. User-headers iteration walks the Map<String, List<String>> by cap-iter — ArrayGet keys[i], if non-null walk values[i] cons-list and fields.append per (key, value) pair. Each pair allocates a cabi_realloc'd scratch for the key bytes (val bytes go to LM[0..val_len] via to_lm; the scratch keeps them non-overlapping so the host reads both correctly). Content-Type is appended after user headers, before the request constructor (which transfers fields ownership). v1 doesn't set Content-Length, so wasmtime-wasi-http defaults to Transfer- Encoding: chunked — the test server includes a small chunked decoder to handle this. Test http_post_put_patch_round_trip_body_and_headers spawns a custom Python server that echoes the request body + surfaces Content-Type (X-Echo-Type) + a custom user header (X-Echo-Custom) back. Aver fires POST/PUT/PATCH in turn; the test asserts each round-trip preserves method, body, content-type, and the user- provided X-Custom header — covering body marshalling, headers iteration, and method dispatch end-to-end. effect_check.rs graduates all six Http.* methods on wasip2; the only remaining wasip2-rejected effects are Tcp.* and HttpServer.* (Phase 3 / 0.19+).
Step K shipped without setting Content-Length, forcing wasmtime- wasi-http to fall back to Transfer-Encoding: chunked. Most modern HTTP servers handle chunked but Python's stdlib BaseHTTPRequestHandler doesn't, which forced the test to inline a chunked decoder. Setting Content-Length explicitly is also more honest HTTP — receivers know the body size up front. Inline int→decimal in __rt_http_request: - body_len = array.len p_body (no copy; the (array i8) carries the byte count directly) - alloc 16-byte cabi_realloc'd scratch - do-while writes digits backwards from buf+16 (always at least one digit so body_len=0 surfaces "0") - fields.append(fields, "Content-Length", digit_ptr, digit_len) Adds 4 i32 locals (cl_body_len, cl_buf, cl_pos, cl_n) at the end of the local table so existing ref-typed indices don't shift. Test server reverts to plain Content-Length read. Also fixes the WASM clippy gate (`-D warnings`): three doc- lazy-continuation lints from Step K's helper docs (proza ze znakami `+` traktowana jako bullet list). Restructured the prose to use commas instead of `+`-separated phrases and added blank-doc-line separators between bullet lists and following prose.
…fused test Three Err paths in the body-marshalling block left the outgoing- request alive after bailing (handle() never runs to transfer ownership), leaking one resource per error: - request.body Err → req leaked - body.write Err → body + req leaked (was dropping body only) - body.finish Err → body gone but req still alive Each path now drops the appropriate resource(s) child-before- parent before emit_err. The drop_outgoing_request slot was already registered for exactly this case. Also makes http_get_surfaces_connection_refused deterministic. The previous version bound to port 0 and dropped the listener, hoping a connect() to the (now-free) port within ~10s would get RST. The kernel can reuse the port before our connect fires, masking the refused signal. Switched to port 1 (well- known tcpmux, never legitimately listening on dev machines) and broadened the assertion to "err=http: <any error-code>" rather than enumerating specific variant names — the test's contract is "the dispatch surfaced a recognised HTTP error, not a panic," not "host emitted exactly connection-refused." Five consecutive runs stable.
Adds an unreleased 0.19.0 entry covering the six Http.* methods on --target wasip2; codename + tagline TBD by user before tag. Updates docs/wasip2.md effect-mapping table — Http.* no longer rejected, only Tcp.* / HttpServer.* remain compile-rejected on the wasip2 target.
8 tasks
jasisz
added a commit
that referenced
this pull request
May 13, 2026
HTTP both ways: client (PR #25) + server (PR #26) on `--target wasip2`. Same source can call out via `Http.{get, head, delete, post, put, patch}` and answer back via `HttpServer.listen` (`--world wasi:http/ proxy --handler <fn>`). Same `.component.wasm` verified portable across Cranelift (wasmtime serve) and V8 (jco serve on Node 22). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
HTTP support for
--target wasip2— direct WIT lowering to wasi:http (no preview1 adapter), all 6 methods, real response headers with multi-value preservation, per-error-code names.Steps
wasmtime-wasi-http, host linker (add_only_http_to_linker_sync),WasiHttpViewimplWasip2ImportSlotHttp.getslots + WIT world conditionalimport wasi:http/outgoing-handler@0.2.4__rt_http_gethelper — URL parse, outgoing-request build, handle/subscribe/poll/future.get pipeline (~700 LoC)Http.geton wasip2, e2e testincoming-response.headers+fields.entriesMap.get+ reverse-iterate-prepend (RFC 6265 Set-Cookie order)error-codediscriminant (39 cases: connection-refused, DNS-timeout, …)Http.head+Http.deletevia shared helper +set-methodslotHttp.post+Http.put+Http.patchwith body marshalling (request.body/body.write/blocking-write/finish) + user headers iteration + Content-TypeTest coverage (
tests/wasip2_http.rs)http_get_returns_status_and_body— status=200 + body length + Content-type lookuphttp_get_preserves_multi_value_header_order— three Set-Cookie + two X-Order headers, asserts emit-order preservedhttp_get_surfaces_connection_refused— bind+drop port, asserts Err message contains connection-refused / connection-failedhttp_head_and_delete_dispatch_correct_method— server tags responses withX-Method; asserts each verb dispatched correctly + HEAD has empty bodyhttp_post_put_patch_round_trip_body_and_headers— body bytes + Content-Type + user-provided X-Custom round-trip through all three body-bearing methodsTest plan
cargo build --features wasip2 --bin avercleantests/wasip2_poc.rs3/3tests/wasip2_stress.rs6/6tests/wasip2_http.rs5/5cargo fmt --checkclean0.19-http-wasip2Out of scope (deferred)
set-scheme(HTTPS)works, but TLS depends on host wasmtime-wasi-http build; not exercised--wasip2 --recordintegration — recording rejected at CLI todayerror-codevariants (DNS-error rcode, internal-error message, …) — only variant name surfaces today