From f92473c1e48fbe6930b455e886455b81a1dd0008 Mon Sep 17 00:00:00 2001 From: markfisher Date: Thu, 13 Aug 2026 20:23:38 -0400 Subject: [PATCH 1/2] initial commit of http-client Signed-off-by: markfisher --- Cargo.lock | 362 +++++++++++++++++- MAINTAINERS.md | 1 + README.md | 3 +- components/http-client/Cargo.toml | 12 + components/http-client/README.md | 23 ++ components/http-client/src/lib.rs | 241 ++++++++++++ .../componentized-http-0.0.0-0/package.wit | 62 +++ components/wit/worlds.wit | 4 + wit/http.wit | 62 +++ 9 files changed, 762 insertions(+), 8 deletions(-) create mode 100644 components/http-client/Cargo.toml create mode 100644 components/http-client/README.md create mode 100644 components/http-client/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d899e55..c6d0195 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,12 +15,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] -name = "client" -version = "0.1.0" +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ - "serde", - "serde_json", - "wit-bindgen", + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] @@ -35,6 +37,76 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -50,12 +122,123 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http-client" +version = "0.1.0" +dependencies = [ + "url", + "wit-bindgen", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -80,6 +263,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "log" version = "0.4.33" @@ -103,6 +292,27 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -144,7 +354,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", - "serde_derive", ] [[package]] @@ -180,6 +389,24 @@ dependencies = [ "zmij", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "syn" version = "2.0.119" @@ -202,12 +429,51 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "wasm-encoder" version = "0.254.0" @@ -248,6 +514,7 @@ version = "0.60.0" source = "git+https://github.com/bytecodealliance/wit-bindgen.git?rev=d92cfd3497a7adfbe70f03886c11def2deab6faa#d92cfd3497a7adfbe70f03886c11def2deab6faa" dependencies = [ "bitflags", + "futures", "wit-bindgen-rust-macro", ] @@ -329,6 +596,89 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/MAINTAINERS.md b/MAINTAINERS.md index b517ca4..38c29fc 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,3 +1,4 @@ # Maintainers * Scott Andrews, [scothis](https://github.com/scothis) +* Mark Fisher, [markfisher](https://github.com/markfisher) diff --git a/README.md b/README.md index 7875903..f4b6065 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,7 @@ make components ### Components -TODO - +- [`http-client`](./components/http-client/) ## Community diff --git a/components/http-client/Cargo.toml b/components/http-client/Cargo.toml new file mode 100644 index 0000000..51745c1 --- /dev/null +++ b/components/http-client/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "http-client" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +url = "2.5" +wit-bindgen = { workspace = true, features = ["async-spawn"] } diff --git a/components/http-client/README.md b/components/http-client/README.md new file mode 100644 index 0000000..79997c2 --- /dev/null +++ b/components/http-client/README.md @@ -0,0 +1,23 @@ +# `http-client` + +A higher-level HTTP client that delegates to `wasi:http/client`. + +## Request Functions + +(each returns `result`) + +- `request(method, url, headers, body, options)` +- `get(url, headers, options)` +- `post(url, headers, body, options)` +- `put(url, headers, body, options)` +- `delete(url, headers, options)` +- `patch(url, headers, body, options)` +- `head(url, headers, options)` +- `options(url, headers, options)` +- `trace(url, headers, options)` +- `query(url, headers, body, options)` + +## The `http-client` World + +- exports `componentized:http/client` +- imports `wasi:http/client@0.3.1` diff --git a/components/http-client/src/lib.rs b/components/http-client/src/lib.rs new file mode 100644 index 0000000..6512d27 --- /dev/null +++ b/components/http-client/src/lib.rs @@ -0,0 +1,241 @@ +use url::Url; + +wit_bindgen::generate!({ + path: "../wit", + world: "http-client", + generate_all, +}); + +use exports::componentized::http::client::{Guest, HttpResponse, Method, RequestOptions}; + +use wasi::http::types::{ + ErrorCode, Fields, Method as WasiMethod, Request as WasiRequest, + RequestOptions as WasiRequestOptions, Response as WasiResponse, Scheme, +}; +use wit_bindgen::rt::async_support::{FutureReader, StreamReader}; + +struct HttpClient; + +impl HttpClient { + async fn request( + method: WasiMethod, + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + options: Option, + ) -> Result { + let request_headers = Fields::new(); + for (name, value) in &headers { + request_headers + .append(name, value.as_bytes()) + .map_err(|e| format!("Invalid request header {name:?}: {e:?}"))?; + } + + let parsed = Url::parse(&url).map_err(|e| format!("Invalid URL: {e}"))?; + let scheme = match parsed.scheme() { + "http" => Scheme::Http, + "https" => Scheme::Https, + other => return Err(format!("Unsupported URL scheme: {other}")), + }; + let host = parsed + .host_str() + .ok_or_else(|| "URL is missing a host".to_string())?; + let authority = match parsed.port() { + Some(port) => format!("{host}:{port}"), + None => host.to_string(), + }; + let path_with_query = match parsed.query() { + Some(q) => format!("{}?{q}", parsed.path()), + None => parsed.path().to_string(), + }; + + let (trailers_tx, trailers_rx) = wit_future::new(|| Ok(None)); + trailers_tx.write(Ok(None)); + + let wasi_options = options.map(wasi_request_options).transpose()?; + let (request, send_result) = + WasiRequest::new(request_headers, Some(body), trailers_rx, wasi_options); + request + .set_method(&method) + .map_err(|()| "Failed to set request method".to_string())?; + request + .set_scheme(Some(&scheme)) + .map_err(|()| "Failed to set request scheme".to_string())?; + request + .set_authority(Some(&authority)) + .map_err(|()| "Failed to set request authority".to_string())?; + request + .set_path_with_query(Some(&path_with_query)) + .map_err(|()| "Failed to set request path".to_string())?; + + let response = wasi::http::client::send(request) + .await + .map_err(|e| format!("HTTP request failed: {e:?}"))?; + + let status = response.get_status_code(); + let headers = read_fields(&response.get_headers()); + + let (body_stream, wasi_trailers) = WasiResponse::consume_body(response, send_result); + + Ok(HttpResponse { + status, + headers, + body: body_stream, + trailers: map_trailers(wasi_trailers), + }) + } +} + +fn read_fields(fields: &Fields) -> Vec<(String, String)> { + fields + .copy_all() + .into_iter() + .map(|(k, v)| (k, v.into_iter().map(|b| b as char).collect())) + .collect() +} + +fn map_trailers( + wasi: FutureReader, ErrorCode>>, +) -> FutureReader, String>> { + let (tx, rx) = wit_future::new(|| Ok(Vec::new())); + wit_bindgen::rt::async_support::spawn_local(async move { + let resolved = match wasi.await { + Ok(Some(t)) => Ok(read_fields(&t)), + Ok(None) => Ok(Vec::new()), + Err(e) => Err(format!("wasi:http error: {e:?}")), + }; + tx.write(resolved); + }); + rx +} + +fn wasi_request_options(opts: RequestOptions) -> Result { + let r = WasiRequestOptions::new(); + if let Some(ms) = opts.connect_timeout_ms { + r.set_connect_timeout(Some(ms_to_ns(ms))) + .map_err(|e| format!("connect-timeout: {e:?}"))?; + } + if let Some(ms) = opts.first_byte_timeout_ms { + r.set_first_byte_timeout(Some(ms_to_ns(ms))) + .map_err(|e| format!("first-byte-timeout: {e:?}"))?; + } + if let Some(ms) = opts.between_bytes_timeout_ms { + r.set_between_bytes_timeout(Some(ms_to_ns(ms))) + .map_err(|e| format!("between-bytes-timeout: {e:?}"))?; + } + Ok(r) +} + +fn ms_to_ns(ms: u32) -> u64 { + u64::from(ms) * 1_000_000 +} + +fn to_wasi_method(method: Method) -> WasiMethod { + match method { + Method::Get => WasiMethod::Get, + Method::Post => WasiMethod::Post, + Method::Put => WasiMethod::Put, + Method::Delete => WasiMethod::Delete, + Method::Patch => WasiMethod::Patch, + Method::Head => WasiMethod::Head, + Method::Options => WasiMethod::Options, + Method::Trace => WasiMethod::Trace, + Method::Query => WasiMethod::Other("QUERY".to_string()), + } +} + +fn empty_body() -> StreamReader { + let (tx, rx) = wit_stream::new::(); + drop(tx); + rx +} + +impl Guest for HttpClient { + async fn request( + method: Method, + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + options: Option, + ) -> Result { + Self::request(to_wasi_method(method), url, headers, body, options).await + } + + async fn get( + url: String, + headers: Vec<(String, String)>, + options: Option, + ) -> Result { + Self::request(WasiMethod::Get, url, headers, empty_body(), options).await + } + + async fn post( + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + options: Option, + ) -> Result { + Self::request(WasiMethod::Post, url, headers, body, options).await + } + + async fn put( + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + options: Option, + ) -> Result { + Self::request(WasiMethod::Put, url, headers, body, options).await + } + + async fn delete( + url: String, + headers: Vec<(String, String)>, + options: Option, + ) -> Result { + Self::request(WasiMethod::Delete, url, headers, empty_body(), options).await + } + + async fn patch( + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + options: Option, + ) -> Result { + Self::request(WasiMethod::Patch, url, headers, body, options).await + } + + async fn head( + url: String, + headers: Vec<(String, String)>, + options: Option, + ) -> Result { + Self::request(WasiMethod::Head, url, headers, empty_body(), options).await + } + + async fn options( + url: String, + headers: Vec<(String, String)>, + options: Option, + ) -> Result { + Self::request(WasiMethod::Options, url, headers, empty_body(), options).await + } + + async fn trace( + url: String, + headers: Vec<(String, String)>, + options: Option, + ) -> Result { + Self::request(WasiMethod::Trace, url, headers, empty_body(), options).await + } + + async fn query( + url: String, + headers: Vec<(String, String)>, + body: StreamReader, + options: Option, + ) -> Result { + Self::request(to_wasi_method(Method::Query), url, headers, body, options).await + } +} + +export!(HttpClient); diff --git a/components/wit/deps/componentized-http-0.0.0-0/package.wit b/components/wit/deps/componentized-http-0.0.0-0/package.wit index f57d9f4..e5b9956 100644 --- a/components/wit/deps/componentized-http-0.0.0-0/package.wit +++ b/components/wit/deps/componentized-http-0.0.0-0/package.wit @@ -1,5 +1,67 @@ package componentized:http@0.0.0-0; +interface client { + enum method { + get, + post, + put, + delete, + patch, + head, + options, + trace, + query, + } + + /// Per-request options. None fields fall through to host defaults. + record request-options { + connect-timeout-ms: option, + first-byte-timeout-ms: option, + between-bytes-timeout-ms: option, + } + + /// A streaming HTTP response. The status and headers are available + /// immediately; the body streams as `body`, and trailers (if any) resolve + /// via `trailers` once the body stream is fully consumed. + record http-response { + status: u16, + headers: list>, + body: stream, + trailers: future>, string>>, + } + + /// Send an HTTP request with an explicit method. Both the request body and + /// the response body stream. + request: async func(method: method, url: string, headers: list>, body: stream, options: option) -> result; + + /// HTTP GET request. + get: async func(url: string, headers: list>, options: option) -> result; + + /// HTTP POST request. + post: async func(url: string, headers: list>, body: stream, options: option) -> result; + + /// HTTP PUT request. + put: async func(url: string, headers: list>, body: stream, options: option) -> result; + + /// HTTP DELETE request. + delete: async func(url: string, headers: list>, options: option) -> result; + + /// HTTP PATCH request. + patch: async func(url: string, headers: list>, body: stream, options: option) -> result; + + /// HTTP HEAD request. + head: async func(url: string, headers: list>, options: option) -> result; + + /// HTTP OPTIONS request. + options: async func(url: string, headers: list>, options: option) -> result; + + /// HTTP TRACE request. + trace: async func(url: string, headers: list>, options: option) -> result; + + /// HTTP QUERY request. + query: async func(url: string, headers: list>, body: stream, options: option) -> result; +} + world imports { import wasi:clocks/types@0.3.1; import wasi:http/types@0.3.1; diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index 654ebda..2130a11 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -1,2 +1,6 @@ package componentized:components; +world http-client { + import wasi:http/client@0.3.1; + export componentized:http/client@0.0.0-0; +} diff --git a/wit/http.wit b/wit/http.wit index af0d33b..0c82053 100644 --- a/wit/http.wit +++ b/wit/http.wit @@ -1 +1,63 @@ package componentized:http@0.0.0-0; + +interface client { + enum method { + get, + post, + put, + delete, + patch, + head, + options, + trace, + query, + } + + /// Per-request options. None fields fall through to host defaults. + record request-options { + connect-timeout-ms: option, + first-byte-timeout-ms: option, + between-bytes-timeout-ms: option, + } + + /// A streaming HTTP response. The status and headers are available + /// immediately; the body streams as `body`, and trailers (if any) resolve + /// via `trailers` once the body stream is fully consumed. + record http-response { + status: u16, + headers: list>, + body: stream, + trailers: future>, string>>, + } + + /// Send an HTTP request with an explicit method. Both the request body and + /// the response body stream. + request: async func(method: method, url: string, headers: list>, body: stream, options: option) -> result; + + /// HTTP GET request. + get: async func(url: string, headers: list>, options: option) -> result; + + /// HTTP POST request. + post: async func(url: string, headers: list>, body: stream, options: option) -> result; + + /// HTTP PUT request. + put: async func(url: string, headers: list>, body: stream, options: option) -> result; + + /// HTTP DELETE request. + delete: async func(url: string, headers: list>, options: option) -> result; + + /// HTTP PATCH request. + patch: async func(url: string, headers: list>, body: stream, options: option) -> result; + + /// HTTP HEAD request. + head: async func(url: string, headers: list>, options: option) -> result; + + /// HTTP OPTIONS request. + options: async func(url: string, headers: list>, options: option) -> result; + + /// HTTP TRACE request. + trace: async func(url: string, headers: list>, options: option) -> result; + + /// HTTP QUERY request. + query: async func(url: string, headers: list>, body: stream, options: option) -> result; +} From 0914e02e653c359987a3e807744a0e35cae84b2d Mon Sep 17 00:00:00 2001 From: markfisher Date: Thu, 13 Aug 2026 21:41:00 -0400 Subject: [PATCH 2/2] address review feedback Signed-off-by: markfisher --- components/http-client/README.md | 2 +- components/http-client/src/lib.rs | 103 ++-- .../componentized-http-0.0.0-0/package.wit | 29 +- wit/deps/wasi-cli-0.3.1/package.wit | 28 - wit/deps/wasi-clocks-0.3.1/package.wit | 43 -- wit/deps/wasi-http-0.3.1/package.wit | 510 ------------------ wit/deps/wasi-random-0.3.1/package.wit | 18 - wit/http.wit | 26 +- wit/worlds.wit | 2 +- wkg.lock | 10 +- 10 files changed, 89 insertions(+), 682 deletions(-) delete mode 100644 wit/deps/wasi-cli-0.3.1/package.wit delete mode 100644 wit/deps/wasi-clocks-0.3.1/package.wit delete mode 100644 wit/deps/wasi-http-0.3.1/package.wit delete mode 100644 wit/deps/wasi-random-0.3.1/package.wit diff --git a/components/http-client/README.md b/components/http-client/README.md index 79997c2..2dcab4f 100644 --- a/components/http-client/README.md +++ b/components/http-client/README.md @@ -1,6 +1,6 @@ # `http-client` -A higher-level HTTP client that delegates to `wasi:http/client`. +A higher-level HTTP client that delegates to wasi:http/client. ## Request Functions diff --git a/components/http-client/src/lib.rs b/components/http-client/src/lib.rs index 6512d27..8cdb852 100644 --- a/components/http-client/src/lib.rs +++ b/components/http-client/src/lib.rs @@ -6,11 +6,13 @@ wit_bindgen::generate!({ generate_all, }); -use exports::componentized::http::client::{Guest, HttpResponse, Method, RequestOptions}; +use exports::componentized::http::client::{ + ErrorCode, Guest, HttpResponse, Method, RequestOptions, +}; use wasi::http::types::{ - ErrorCode, Fields, Method as WasiMethod, Request as WasiRequest, - RequestOptions as WasiRequestOptions, Response as WasiResponse, Scheme, + ErrorCode as WasiErrorCode, Fields, Method as WasiMethod, Request as WasiRequest, + RequestOptions as WasiRequestOptions, Response as WasiResponse, Scheme, Trailers, }; use wit_bindgen::rt::async_support::{FutureReader, StreamReader}; @@ -21,25 +23,25 @@ impl HttpClient { method: WasiMethod, url: String, headers: Vec<(String, String)>, - body: StreamReader, + body: Option>, options: Option, - ) -> Result { + ) -> Result { let request_headers = Fields::new(); for (name, value) in &headers { request_headers .append(name, value.as_bytes()) - .map_err(|e| format!("Invalid request header {name:?}: {e:?}"))?; + .map_err(|e| err(format!("Invalid request header {name:?}: {e:?}")))?; } - let parsed = Url::parse(&url).map_err(|e| format!("Invalid URL: {e}"))?; + let parsed = Url::parse(&url).map_err(|e| err(format!("Invalid URL: {e}")))?; let scheme = match parsed.scheme() { "http" => Scheme::Http, "https" => Scheme::Https, - other => return Err(format!("Unsupported URL scheme: {other}")), + other => return Err(err(format!("Unsupported URL scheme: {other}"))), }; let host = parsed .host_str() - .ok_or_else(|| "URL is missing a host".to_string())?; + .ok_or_else(|| err("URL is missing a host".to_string()))?; let authority = match parsed.port() { Some(port) => format!("{host}:{port}"), None => host.to_string(), @@ -54,23 +56,23 @@ impl HttpClient { let wasi_options = options.map(wasi_request_options).transpose()?; let (request, send_result) = - WasiRequest::new(request_headers, Some(body), trailers_rx, wasi_options); + WasiRequest::new(request_headers, body, trailers_rx, wasi_options); request .set_method(&method) - .map_err(|()| "Failed to set request method".to_string())?; + .map_err(|()| err("Failed to set request method".to_string()))?; request .set_scheme(Some(&scheme)) - .map_err(|()| "Failed to set request scheme".to_string())?; + .map_err(|()| err("Failed to set request scheme".to_string()))?; request .set_authority(Some(&authority)) - .map_err(|()| "Failed to set request authority".to_string())?; + .map_err(|()| err("Failed to set request authority".to_string()))?; request .set_path_with_query(Some(&path_with_query)) - .map_err(|()| "Failed to set request path".to_string())?; + .map_err(|()| err("Failed to set request path".to_string()))?; let response = wasi::http::client::send(request) .await - .map_err(|e| format!("HTTP request failed: {e:?}"))?; + .map_err(|e| err(format!("HTTP request failed: {e:?}")))?; let status = response.get_status_code(); let headers = read_fields(&response.get_headers()); @@ -86,6 +88,10 @@ impl HttpClient { } } +fn err(message: String) -> ErrorCode { + ErrorCode::Other(Some(message)) +} + fn read_fields(fields: &Fields) -> Vec<(String, String)> { fields .copy_all() @@ -95,33 +101,33 @@ fn read_fields(fields: &Fields) -> Vec<(String, String)> { } fn map_trailers( - wasi: FutureReader, ErrorCode>>, -) -> FutureReader, String>> { + wasi: FutureReader, WasiErrorCode>>, +) -> FutureReader, ErrorCode>> { let (tx, rx) = wit_future::new(|| Ok(Vec::new())); wit_bindgen::rt::async_support::spawn_local(async move { let resolved = match wasi.await { Ok(Some(t)) => Ok(read_fields(&t)), Ok(None) => Ok(Vec::new()), - Err(e) => Err(format!("wasi:http error: {e:?}")), + Err(e) => Err(err(format!("wasi:http error: {e:?}"))), }; tx.write(resolved); }); rx } -fn wasi_request_options(opts: RequestOptions) -> Result { +fn wasi_request_options(opts: RequestOptions) -> Result { let r = WasiRequestOptions::new(); if let Some(ms) = opts.connect_timeout_ms { r.set_connect_timeout(Some(ms_to_ns(ms))) - .map_err(|e| format!("connect-timeout: {e:?}"))?; + .map_err(|e| err(format!("connect-timeout: {e:?}")))?; } if let Some(ms) = opts.first_byte_timeout_ms { r.set_first_byte_timeout(Some(ms_to_ns(ms))) - .map_err(|e| format!("first-byte-timeout: {e:?}"))?; + .map_err(|e| err(format!("first-byte-timeout: {e:?}")))?; } if let Some(ms) = opts.between_bytes_timeout_ms { r.set_between_bytes_timeout(Some(ms_to_ns(ms))) - .map_err(|e| format!("between-bytes-timeout: {e:?}"))?; + .map_err(|e| err(format!("between-bytes-timeout: {e:?}")))?; } Ok(r) } @@ -144,20 +150,14 @@ fn to_wasi_method(method: Method) -> WasiMethod { } } -fn empty_body() -> StreamReader { - let (tx, rx) = wit_stream::new::(); - drop(tx); - rx -} - impl Guest for HttpClient { async fn request( method: Method, url: String, headers: Vec<(String, String)>, - body: StreamReader, + body: Option>, options: Option, - ) -> Result { + ) -> Result { Self::request(to_wasi_method(method), url, headers, body, options).await } @@ -165,8 +165,8 @@ impl Guest for HttpClient { url: String, headers: Vec<(String, String)>, options: Option, - ) -> Result { - Self::request(WasiMethod::Get, url, headers, empty_body(), options).await + ) -> Result { + Self::request(WasiMethod::Get, url, headers, None, options).await } async fn post( @@ -174,8 +174,8 @@ impl Guest for HttpClient { headers: Vec<(String, String)>, body: StreamReader, options: Option, - ) -> Result { - Self::request(WasiMethod::Post, url, headers, body, options).await + ) -> Result { + Self::request(WasiMethod::Post, url, headers, Some(body), options).await } async fn put( @@ -183,16 +183,16 @@ impl Guest for HttpClient { headers: Vec<(String, String)>, body: StreamReader, options: Option, - ) -> Result { - Self::request(WasiMethod::Put, url, headers, body, options).await + ) -> Result { + Self::request(WasiMethod::Put, url, headers, Some(body), options).await } async fn delete( url: String, headers: Vec<(String, String)>, options: Option, - ) -> Result { - Self::request(WasiMethod::Delete, url, headers, empty_body(), options).await + ) -> Result { + Self::request(WasiMethod::Delete, url, headers, None, options).await } async fn patch( @@ -200,32 +200,32 @@ impl Guest for HttpClient { headers: Vec<(String, String)>, body: StreamReader, options: Option, - ) -> Result { - Self::request(WasiMethod::Patch, url, headers, body, options).await + ) -> Result { + Self::request(WasiMethod::Patch, url, headers, Some(body), options).await } async fn head( url: String, headers: Vec<(String, String)>, options: Option, - ) -> Result { - Self::request(WasiMethod::Head, url, headers, empty_body(), options).await + ) -> Result { + Self::request(WasiMethod::Head, url, headers, None, options).await } async fn options( url: String, headers: Vec<(String, String)>, options: Option, - ) -> Result { - Self::request(WasiMethod::Options, url, headers, empty_body(), options).await + ) -> Result { + Self::request(WasiMethod::Options, url, headers, None, options).await } async fn trace( url: String, headers: Vec<(String, String)>, options: Option, - ) -> Result { - Self::request(WasiMethod::Trace, url, headers, empty_body(), options).await + ) -> Result { + Self::request(WasiMethod::Trace, url, headers, None, options).await } async fn query( @@ -233,8 +233,15 @@ impl Guest for HttpClient { headers: Vec<(String, String)>, body: StreamReader, options: Option, - ) -> Result { - Self::request(to_wasi_method(Method::Query), url, headers, body, options).await + ) -> Result { + Self::request( + to_wasi_method(Method::Query), + url, + headers, + Some(body), + options, + ) + .await } } diff --git a/components/wit/deps/componentized-http-0.0.0-0/package.wit b/components/wit/deps/componentized-http-0.0.0-0/package.wit index e5b9956..3cbee81 100644 --- a/components/wit/deps/componentized-http-0.0.0-0/package.wit +++ b/components/wit/deps/componentized-http-0.0.0-0/package.wit @@ -13,6 +13,10 @@ interface client { query, } + variant error-code { + other(option), + } + /// Per-request options. None fields fall through to host defaults. record request-options { connect-timeout-ms: option, @@ -27,42 +31,41 @@ interface client { status: u16, headers: list>, body: stream, - trailers: future>, string>>, + trailers: future>, error-code>>, } /// Send an HTTP request with an explicit method. Both the request body and /// the response body stream. - request: async func(method: method, url: string, headers: list>, body: stream, options: option) -> result; + request: async func(method: method, url: string, headers: list>, body: option>, options: option) -> result; /// HTTP GET request. - get: async func(url: string, headers: list>, options: option) -> result; + get: async func(url: string, headers: list>, options: option) -> result; /// HTTP POST request. - post: async func(url: string, headers: list>, body: stream, options: option) -> result; + post: async func(url: string, headers: list>, body: stream, options: option) -> result; /// HTTP PUT request. - put: async func(url: string, headers: list>, body: stream, options: option) -> result; + put: async func(url: string, headers: list>, body: stream, options: option) -> result; /// HTTP DELETE request. - delete: async func(url: string, headers: list>, options: option) -> result; + delete: async func(url: string, headers: list>, options: option) -> result; /// HTTP PATCH request. - patch: async func(url: string, headers: list>, body: stream, options: option) -> result; + patch: async func(url: string, headers: list>, body: stream, options: option) -> result; /// HTTP HEAD request. - head: async func(url: string, headers: list>, options: option) -> result; + head: async func(url: string, headers: list>, options: option) -> result; /// HTTP OPTIONS request. - options: async func(url: string, headers: list>, options: option) -> result; + options: async func(url: string, headers: list>, options: option) -> result; /// HTTP TRACE request. - trace: async func(url: string, headers: list>, options: option) -> result; + trace: async func(url: string, headers: list>, options: option) -> result; /// HTTP QUERY request. - query: async func(url: string, headers: list>, body: stream, options: option) -> result; + query: async func(url: string, headers: list>, body: stream, options: option) -> result; } world imports { - import wasi:clocks/types@0.3.1; - import wasi:http/types@0.3.1; + import client; } diff --git a/wit/deps/wasi-cli-0.3.1/package.wit b/wit/deps/wasi-cli-0.3.1/package.wit deleted file mode 100644 index ff51560..0000000 --- a/wit/deps/wasi-cli-0.3.1/package.wit +++ /dev/null @@ -1,28 +0,0 @@ -package wasi:cli@0.3.1; - -interface types { - enum error-code { - io, - illegal-byte-sequence, - pipe, - } -} - -interface stdout { - use types.{error-code}; - - write-via-stream: func(data: stream) -> future>; -} - -interface stderr { - use types.{error-code}; - - write-via-stream: func(data: stream) -> future>; -} - -interface stdin { - use types.{error-code}; - - read-via-stream: func() -> tuple, future>>; -} - diff --git a/wit/deps/wasi-clocks-0.3.1/package.wit b/wit/deps/wasi-clocks-0.3.1/package.wit deleted file mode 100644 index 611b0f1..0000000 --- a/wit/deps/wasi-clocks-0.3.1/package.wit +++ /dev/null @@ -1,43 +0,0 @@ -package wasi:clocks@0.3.1; - -interface types { - type duration = u64; -} - -interface monotonic-clock { - use types.{duration}; - - type mark = u64; - - now: func() -> mark; - - get-resolution: func() -> duration; - - wait-until: async func(when: mark); - - wait-for: async func(how-long: duration); -} - -interface system-clock { - use types.{duration}; - - record instant { - seconds: s64, - nanoseconds: u32, - } - - now: func() -> instant; - - get-resolution: func() -> duration; -} - -interface timezone { - use system-clock.{instant}; - - iana-id: func() -> option; - - utc-offset: func(when: instant) -> option; - - to-debug-string: func() -> string; -} - diff --git a/wit/deps/wasi-http-0.3.1/package.wit b/wit/deps/wasi-http-0.3.1/package.wit deleted file mode 100644 index 3e6aa0c..0000000 --- a/wit/deps/wasi-http-0.3.1/package.wit +++ /dev/null @@ -1,510 +0,0 @@ -package wasi:http@0.3.1; - -/// This interface defines all of the types and methods for implementing HTTP -/// Requests and Responses, as well as their headers, trailers, and bodies. -@since(version = 0.3.0) -interface types { - use wasi:clocks/types@0.3.1.{duration}; - - /// This type corresponds to HTTP standard Methods. - @since(version = 0.3.0) - variant method { - get, - head, - post, - put, - delete, - connect, - options, - trace, - patch, - other(string), - } - - /// This type corresponds to HTTP standard Related Schemes. - @since(version = 0.3.0) - variant scheme { - HTTP, - HTTPS, - other(string), - } - - /// Defines the case payload type for `DNS-error` above: - @since(version = 0.3.0) - record DNS-error-payload { - rcode: option, - info-code: option, - } - - /// Defines the case payload type for `TLS-alert-received` above: - @since(version = 0.3.0) - record TLS-alert-received-payload { - alert-id: option, - alert-message: option, - } - - /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: - @since(version = 0.3.0) - record field-size-payload { - field-name: option, - field-size: option, - } - - /// These cases are inspired by the IANA HTTP Proxy Error Types: - /// - @since(version = 0.3.0) - variant error-code { - DNS-timeout, - DNS-error(DNS-error-payload), - destination-not-found, - destination-unavailable, - destination-IP-prohibited, - destination-IP-unroutable, - connection-refused, - connection-terminated, - connection-timeout, - connection-read-timeout, - connection-write-timeout, - connection-limit-reached, - TLS-protocol-error, - TLS-certificate-error, - TLS-alert-received(TLS-alert-received-payload), - HTTP-request-denied, - HTTP-request-length-required, - HTTP-request-body-size(option), - HTTP-request-method-invalid, - HTTP-request-URI-invalid, - HTTP-request-URI-too-long, - HTTP-request-header-section-size(option), - HTTP-request-header-size(option), - HTTP-request-trailer-section-size(option), - HTTP-request-trailer-size(field-size-payload), - HTTP-response-incomplete, - HTTP-response-header-section-size(option), - HTTP-response-header-size(field-size-payload), - HTTP-response-body-size(option), - HTTP-response-trailer-section-size(option), - HTTP-response-trailer-size(field-size-payload), - HTTP-response-transfer-coding(option), - HTTP-response-content-coding(option), - HTTP-response-timeout, - HTTP-upgrade-failed, - HTTP-protocol-error, - loop-detected, - configuration-error, - /// This is a catch-all error for anything that doesn't fit cleanly into a - /// more specific case. It also includes an optional string for an - /// unstructured description of the error. Users should not depend on the - /// string for diagnosing errors, as it's not required to be consistent - /// between implementations. - internal-error(option), - } - - /// This type enumerates the different kinds of errors that may occur when - /// setting or appending to a `fields` resource. - @since(version = 0.3.0) - variant header-error { - /// This error indicates that a `field-name` or `field-value` was - /// syntactically invalid when used with an operation that sets headers in a - /// `fields`. - invalid-syntax, - /// This error indicates that a forbidden `field-name` was used when trying - /// to set a header in a `fields`. - forbidden, - /// This error indicates that the operation on the `fields` was not - /// permitted because the fields are immutable. - immutable, - /// This error indicates that the operation would exceed an - /// implementation-defined limit on field sizes. This may apply to - /// an individual `field-value`, a single `field-name` plus all its - /// values, or the total aggregate size of all fields. - size-exceeded, - /// This is a catch-all error for anything that doesn't fit cleanly into a - /// more specific case. Implementations can use this to extend the error - /// type without breaking existing code. It also includes an optional - /// string for an unstructured description of the error. Users should not - /// depend on the string for diagnosing errors, as it's not required to be - /// consistent between implementations. - other(option), - } - - /// This type enumerates the different kinds of errors that may occur when - /// setting fields of a `request-options` resource. - @since(version = 0.3.0) - variant request-options-error { - /// Indicates the specified field is not supported by this implementation. - not-supported, - /// Indicates that the operation on the `request-options` was not permitted - /// because it is immutable. - immutable, - /// This is a catch-all error for anything that doesn't fit cleanly into a - /// more specific case. Implementations can use this to extend the error - /// type without breaking existing code. It also includes an optional - /// string for an unstructured description of the error. Users should not - /// depend on the string for diagnosing errors, as it's not required to be - /// consistent between implementations. - other(option), - } - - /// Field names are always strings. - /// - /// Field names should always be treated as case insensitive by the `fields` - /// resource for the purposes of equality checking. - @since(version = 0.3.0) - type field-name = string; - - /// Field values should always be ASCII strings. However, in - /// reality, HTTP implementations often have to interpret malformed values, - /// so they are provided as a list of bytes. - @since(version = 0.3.0) - type field-value = list; - - /// This following block defines the `fields` resource which corresponds to - /// HTTP standard Fields. Fields are a common representation used for both - /// Headers and Trailers. - /// - /// A `fields` may be mutable or immutable. A `fields` created using the - /// constructor, `from-list`, or `clone` will be mutable, but a `fields` - /// resource given by other means (including, but not limited to, - /// `request.headers`) might be be immutable. In an immutable fields, the - /// `set`, `append`, and `delete` operations will fail with - /// `header-error.immutable`. - /// - /// A `fields` resource should store `field-name`s and `field-value`s in their - /// original casing used to construct or mutate the `fields` resource. The `fields` - /// resource should use that original casing when returning them from a method. - /// The `fields` resource may use a different casing when serializing the value - /// for transmission, as doing so may improve encoding efficiency. - /// - /// Implementations may impose limits on individual field values and on total - /// aggregate field section size. Operations that would exceed these limits - /// fail with `header-error.size-exceeded` - @since(version = 0.3.0) - resource fields { - /// Construct an empty HTTP Fields. - /// - /// The resulting `fields` is mutable. - constructor(); - /// Construct an HTTP Fields. - /// - /// The resulting `fields` is mutable. - /// - /// The list represents each name-value pair in the Fields. Names - /// which have multiple values are represented by multiple entries in this - /// list with the same name. - /// - /// The tuple is a pair of the field name, represented as a string, and - /// Value, represented as a list of bytes. In a valid Fields, all names - /// and values are valid UTF-8 strings. However, values are not always - /// well-formed, so they are represented as a raw list of bytes. - /// - /// An error result will be returned if any header or value was - /// syntactically invalid, if a header was forbidden, or if the - /// entries would exceed an implementation size limit. - from-list: static func(entries: list>) -> result; - /// Get all of the values corresponding to a name. If the name is not present - /// in this `fields`, an empty list is returned. However, if the name is - /// present but empty, this is represented by a list with one or more - /// empty field-values present. - get: func(name: field-name) -> list; - /// Returns `true` when the name is present in this `fields`. If the name is - /// syntactically invalid, `false` is returned. - has: func(name: field-name) -> bool; - /// Set all of the values for a name. Clears any existing values for that - /// name, if they have been set. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.size-exceeded` if the name or values would - /// exceed an implementation-defined size limit. - set: func(name: field-name, value: list) -> result<_, header-error>; - /// Delete all values for a name. Does nothing if no values for the name - /// exist. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - delete: func(name: field-name) -> result<_, header-error>; - /// Delete all values for a name. Does nothing if no values for the name - /// exist. - /// - /// Returns all values previously corresponding to the name, if any. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - get-and-delete: func(name: field-name) -> result, header-error>; - /// Append a value for a name. Does not change or delete any existing - /// values for that name. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.size-exceeded` if the value would exceed - /// an implementation-defined size limit. - append: func(name: field-name, value: field-value) -> result<_, header-error>; - /// Retrieve the full set of names and values in the Fields. Like the - /// constructor, the list represents each name-value pair. - /// - /// The outer list represents each name-value pair in the Fields. Names - /// which have multiple values are represented by multiple entries in this - /// list with the same name. - /// - /// The names and values are always returned in the original casing and in - /// the order in which they will be serialized for transport. - copy-all: func() -> list>; - /// Make a deep copy of the Fields. Equivalent in behavior to calling the - /// `fields` constructor on the return value of `copy-all`. The resulting - /// `fields` is mutable. - clone: func() -> fields; - } - - /// Headers is an alias for Fields. - @since(version = 0.3.0) - type headers = fields; - - /// Trailers is an alias for Fields. - @since(version = 0.3.0) - type trailers = fields; - - /// Represents an HTTP Request. - @since(version = 0.3.0) - resource request { - /// Construct a new `request` with a default `method` of `GET`, and - /// `none` values for `path-with-query`, `scheme`, and `authority`. - /// - /// `headers` is the HTTP Headers for the Request. - /// - /// `contents` is the optional body content stream with `none` - /// representing a zero-length content stream. - /// Once it is closed, `trailers` future must resolve to a result. - /// If `trailers` resolves to an error, underlying connection - /// will be closed immediately. - /// - /// `options` is optional `request-options` resource to be used - /// if the request is sent over a network connection. - /// - /// It is possible to construct, or manipulate with the accessor functions - /// below, a `request` with an invalid combination of `scheme` - /// and `authority`, or `headers` which are not permitted to be sent. - /// It is the obligation of the `handler.handle` implementation - /// to reject invalid constructions of `request`. - /// - /// The returned future resolves to result of transmission of this request. - new: static func(headers: headers, contents: option>, trailers: future, error-code>>, options: option) -> tuple>>; - /// Get the Method for the Request. - get-method: func() -> method; - /// Set the Method for the Request. Fails if the string present in a - /// `method.other` argument is not a syntactically valid method. - set-method: func(method: method) -> result; - /// Get the combination of the HTTP Path and Query for the Request. When - /// `none`, this represents an empty Path and empty Query. - get-path-with-query: func() -> option; - /// Set the combination of the HTTP Path and Query for the Request. When - /// `none`, this represents an empty Path and empty Query. Fails is the - /// string given is not a syntactically valid path and query uri component. - set-path-with-query: func(path-with-query: option) -> result; - /// Get the HTTP Related Scheme for the Request. When `none`, the - /// implementation may choose an appropriate default scheme. - get-scheme: func() -> option; - /// Set the HTTP Related Scheme for the Request. When `none`, the - /// implementation may choose an appropriate default scheme. Fails if the - /// string given is not a syntactically valid uri scheme. - set-scheme: func(scheme: option) -> result; - /// Get the authority of the Request's target URI. A value of `none` may be used - /// with Related Schemes which do not require an authority. The HTTP and - /// HTTPS schemes always require an authority. - get-authority: func() -> option; - /// Set the authority of the Request's target URI. A value of `none` may be used - /// with Related Schemes which do not require an authority. The HTTP and - /// HTTPS schemes always require an authority. Fails if the string given is - /// not a syntactically valid URI authority. - set-authority: func(authority: option) -> result; - /// Get the `request-options` to be associated with this request - /// - /// The returned `request-options` resource is immutable: `set-*` operations - /// will fail if invoked. - /// - /// This `request-options` resource is a child: it must be dropped before - /// the parent `request` is dropped, or its ownership is transferred to - /// another component by e.g. `handler.handle`. - get-options: func() -> option; - /// Get the headers associated with the Request. - /// - /// The returned `headers` resource is immutable: `set`, `append`, and - /// `delete` operations will fail with `header-error.immutable`. - get-headers: func() -> headers; - /// Get body of the Request. - /// - /// Stream returned by this method represents the contents of the body. - /// Once the stream is reported as closed, callers should await the returned - /// future to determine whether the body was received successfully. - /// The future will only resolve after the stream is reported as closed. - /// - /// This function takes a `res` future as a parameter, which can be used to - /// communicate an error in handling of the request. - /// - /// Note that function will move the `request`, but references to headers or - /// request options acquired from it previously will remain valid. - consume-body: static func(this: request, res: future>) -> tuple, future, error-code>>>; - } - - /// Parameters for making an HTTP Request. Each of these parameters is - /// currently an optional timeout applicable to the transport layer of the - /// HTTP protocol. - /// - /// These timeouts are separate from any the user may use to bound an - /// asynchronous call. - @since(version = 0.3.0) - resource request-options { - /// Construct a default `request-options` value. - constructor(); - /// The timeout for the initial connect to the HTTP Server. - get-connect-timeout: func() -> option; - /// Set the timeout for the initial connect to the HTTP Server. An error - /// return value indicates that this timeout is not supported or that this - /// handle is immutable. - set-connect-timeout: func(duration: option) -> result<_, request-options-error>; - /// The timeout for receiving the first byte of the Response body. - get-first-byte-timeout: func() -> option; - /// Set the timeout for receiving the first byte of the Response body. An - /// error return value indicates that this timeout is not supported or that - /// this handle is immutable. - set-first-byte-timeout: func(duration: option) -> result<_, request-options-error>; - /// The timeout for receiving subsequent chunks of bytes in the Response - /// body stream. - get-between-bytes-timeout: func() -> option; - /// Set the timeout for receiving subsequent chunks of bytes in the Response - /// body stream. An error return value indicates that this timeout is not - /// supported or that this handle is immutable. - set-between-bytes-timeout: func(duration: option) -> result<_, request-options-error>; - /// Make a deep copy of the `request-options`. - /// The resulting `request-options` is mutable. - clone: func() -> request-options; - } - - /// This type corresponds to the HTTP standard Status Code. - @since(version = 0.3.0) - type status-code = u16; - - /// Represents an HTTP Response. - @since(version = 0.3.0) - resource response { - /// Construct a new `response`, with a default `status-code` of `200`. - /// If a different `status-code` is needed, it must be set via the - /// `set-status-code` method. - /// - /// `headers` is the HTTP Headers for the Response. - /// - /// `contents` is the optional body content stream with `none` - /// representing a zero-length content stream. - /// Once it is closed, `trailers` future must resolve to a result. - /// If `trailers` resolves to an error, underlying connection - /// will be closed immediately. - /// - /// The returned future resolves to result of transmission of this response. - new: static func(headers: headers, contents: option>, trailers: future, error-code>>) -> tuple>>; - /// Get the HTTP Status Code for the Response. - get-status-code: func() -> status-code; - /// Set the HTTP Status Code for the Response. Fails if the status-code - /// given is not a valid http status code. - set-status-code: func(status-code: status-code) -> result; - /// Get the headers associated with the Response. - /// - /// The returned `headers` resource is immutable: `set`, `append`, and - /// `delete` operations will fail with `header-error.immutable`. - get-headers: func() -> headers; - /// Get body of the Response. - /// - /// Stream returned by this method represents the contents of the body. - /// Once the stream is reported as closed, callers should await the returned - /// future to determine whether the body was received successfully. - /// The future will only resolve after the stream is reported as closed. - /// - /// This function takes a `res` future as a parameter, which can be used to - /// communicate an error in handling of the response. - /// - /// Note that function will move the `response`, but references to headers - /// acquired from it previously will remain valid. - consume-body: static func(this: response, res: future>) -> tuple, future, error-code>>>; - } -} - -/// This interface defines a handler of HTTP Requests. -/// -/// In a `wasi:http/service` this interface is exported to respond to an -/// incoming HTTP Request with a Response. -/// -/// In `wasi:http/middleware` this interface is both exported and imported as -/// the "downstream" and "upstream" directions of the middleware chain. -@since(version = 0.3.0) -interface handler { - use types.{request, response, error-code}; - - /// This function may be called with either an incoming request read from the - /// network or a request synthesized or forwarded by another component. - handle: async func(request: request) -> result; -} - -/// This interface defines an HTTP client for sending "outgoing" requests. -/// -/// Most components are expected to import this interface to provide the -/// capability to send HTTP requests to arbitrary destinations on a network. -/// -/// The type signature of `client.send` is the same as `handler.handle`. This -/// duplication is currently necessary because some Component Model tooling -/// (including WIT itself) is unable to represent a component importing two -/// instances of the same interface. A `client.send` import may be linked -/// directly to a `handler.handle` export to bypass the network. -@since(version = 0.3.0) -interface client { - use types.{request, response, error-code}; - - /// This function may be used to either send an outgoing request over the - /// network or to forward it to another component. - send: async func(request: request) -> result; -} - -/// The `wasi:http/service` world captures a broad category of HTTP services -/// including web applications, API servers, and proxies. It may be `include`d -/// in more specific worlds such as `wasi:http/middleware`. -@since(version = 0.3.0) -world service { - import wasi:cli/types@0.3.1; - import wasi:cli/stdout@0.3.1; - import wasi:cli/stderr@0.3.1; - import wasi:cli/stdin@0.3.1; - import wasi:clocks/types@0.3.1; - import types; - import client; - import wasi:clocks/monotonic-clock@0.3.1; - import wasi:clocks/system-clock@0.3.1; - @unstable(feature = clocks-timezone) - import wasi:clocks/timezone@0.3.1; - import wasi:random/random@0.3.1; - import wasi:random/insecure@0.3.1; - import wasi:random/insecure-seed@0.3.1; - - export handler; -} -/// The `wasi:http/middleware` world captures HTTP services that forward HTTP -/// Requests to another handler. -/// -/// Components may implement this world to allow them to participate in handler -/// "chains" where a `request` flows through handlers on its way to some terminal -/// `service` and corresponding `response` flows in the opposite direction. -@since(version = 0.3.0) -world middleware { - import wasi:clocks/types@0.3.1; - import types; - import handler; - import wasi:cli/types@0.3.1; - import wasi:cli/stdout@0.3.1; - import wasi:cli/stderr@0.3.1; - import wasi:cli/stdin@0.3.1; - import client; - import wasi:clocks/monotonic-clock@0.3.1; - import wasi:clocks/system-clock@0.3.1; - @unstable(feature = clocks-timezone) - import wasi:clocks/timezone@0.3.1; - import wasi:random/random@0.3.1; - import wasi:random/insecure@0.3.1; - import wasi:random/insecure-seed@0.3.1; - - export handler; -} diff --git a/wit/deps/wasi-random-0.3.1/package.wit b/wit/deps/wasi-random-0.3.1/package.wit deleted file mode 100644 index 2e94458..0000000 --- a/wit/deps/wasi-random-0.3.1/package.wit +++ /dev/null @@ -1,18 +0,0 @@ -package wasi:random@0.3.1; - -interface random { - get-random-bytes: func(max-len: u64) -> list; - - get-random-u64: func() -> u64; -} - -interface insecure { - get-insecure-random-bytes: func(max-len: u64) -> list; - - get-insecure-random-u64: func() -> u64; -} - -interface insecure-seed { - get-insecure-seed: func() -> tuple; -} - diff --git a/wit/http.wit b/wit/http.wit index 0c82053..0986c1c 100644 --- a/wit/http.wit +++ b/wit/http.wit @@ -13,6 +13,10 @@ interface client { query, } + variant error-code { + other(option), + } + /// Per-request options. None fields fall through to host defaults. record request-options { connect-timeout-ms: option, @@ -27,37 +31,37 @@ interface client { status: u16, headers: list>, body: stream, - trailers: future>, string>>, + trailers: future>, error-code>>, } /// Send an HTTP request with an explicit method. Both the request body and /// the response body stream. - request: async func(method: method, url: string, headers: list>, body: stream, options: option) -> result; + request: async func(method: method, url: string, headers: list>, body: option>, options: option) -> result; /// HTTP GET request. - get: async func(url: string, headers: list>, options: option) -> result; + get: async func(url: string, headers: list>, options: option) -> result; /// HTTP POST request. - post: async func(url: string, headers: list>, body: stream, options: option) -> result; + post: async func(url: string, headers: list>, body: stream, options: option) -> result; /// HTTP PUT request. - put: async func(url: string, headers: list>, body: stream, options: option) -> result; + put: async func(url: string, headers: list>, body: stream, options: option) -> result; /// HTTP DELETE request. - delete: async func(url: string, headers: list>, options: option) -> result; + delete: async func(url: string, headers: list>, options: option) -> result; /// HTTP PATCH request. - patch: async func(url: string, headers: list>, body: stream, options: option) -> result; + patch: async func(url: string, headers: list>, body: stream, options: option) -> result; /// HTTP HEAD request. - head: async func(url: string, headers: list>, options: option) -> result; + head: async func(url: string, headers: list>, options: option) -> result; /// HTTP OPTIONS request. - options: async func(url: string, headers: list>, options: option) -> result; + options: async func(url: string, headers: list>, options: option) -> result; /// HTTP TRACE request. - trace: async func(url: string, headers: list>, options: option) -> result; + trace: async func(url: string, headers: list>, options: option) -> result; /// HTTP QUERY request. - query: async func(url: string, headers: list>, body: stream, options: option) -> result; + query: async func(url: string, headers: list>, body: stream, options: option) -> result; } diff --git a/wit/worlds.wit b/wit/worlds.wit index f5094e0..e520f1f 100644 --- a/wit/worlds.wit +++ b/wit/worlds.wit @@ -1,3 +1,3 @@ world imports { - import wasi:http/types@0.3.1; + import client; } diff --git a/wkg.lock b/wkg.lock index 34355b3..2ad4aa8 100644 --- a/wkg.lock +++ b/wkg.lock @@ -1,12 +1,4 @@ # This file is automatically generated. # It is not intended for manual editing. version = 1 - -[[packages]] -name = "wasi:http" -registry = "wasi.dev" - -[[packages.versions]] -requirement = "=0.3.1" -version = "0.3.1" -digest = "sha256:fac6e40c9b8101cb0a32c76ae63271ffb7c9a66007214d57f3ae8048844d2b5f" +packages = []