From 4142f9676e2fa284d5f527c744f5194da45645de Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Wed, 15 Oct 2025 14:05:53 -0600 Subject: [PATCH 1/5] Add support for WASI P3 Signed-off-by: Brian Hardock --- Cargo.lock | 453 ++++++++++++++++--- Cargo.toml | 22 +- crates/spin-wasip3-http-macro/Cargo.toml | 21 + crates/spin-wasip3-http-macro/src/lib.rs | 37 ++ crates/spin-wasip3-http/Cargo.toml | 18 + crates/spin-wasip3-http/src/lib.rs | 417 +++++++++++++++++ crates/wasip3-http-ext/Cargo.toml | 17 + crates/wasip3-http-ext/src/body_writer.rs | 144 ++++++ crates/wasip3-http-ext/src/lib.rs | 365 +++++++++++++++ examples/wasip3-http-axum-router/.gitignore | 2 + examples/wasip3-http-axum-router/Cargo.toml | 13 + examples/wasip3-http-axum-router/README.md | 9 + examples/wasip3-http-axum-router/spin.toml | 20 + examples/wasip3-http-axum-router/src/lib.rs | 51 +++ examples/wasip3-http-hello-world/Cargo.toml | 10 + examples/wasip3-http-hello-world/spin.toml | 20 + examples/wasip3-http-hello-world/src/lib.rs | 7 + examples/wasip3-http-send-request/Cargo.toml | 13 + examples/wasip3-http-send-request/spin.toml | 23 + examples/wasip3-http-send-request/src/lib.rs | 10 + src/lib.rs | 9 + 21 files changed, 1603 insertions(+), 78 deletions(-) create mode 100644 crates/spin-wasip3-http-macro/Cargo.toml create mode 100644 crates/spin-wasip3-http-macro/src/lib.rs create mode 100644 crates/spin-wasip3-http/Cargo.toml create mode 100644 crates/spin-wasip3-http/src/lib.rs create mode 100644 crates/wasip3-http-ext/Cargo.toml create mode 100644 crates/wasip3-http-ext/src/body_writer.rs create mode 100644 crates/wasip3-http-ext/src/lib.rs create mode 100644 examples/wasip3-http-axum-router/.gitignore create mode 100644 examples/wasip3-http-axum-router/Cargo.toml create mode 100644 examples/wasip3-http-axum-router/README.md create mode 100644 examples/wasip3-http-axum-router/spin.toml create mode 100644 examples/wasip3-http-axum-router/src/lib.rs create mode 100644 examples/wasip3-http-hello-world/Cargo.toml create mode 100644 examples/wasip3-http-hello-world/spin.toml create mode 100644 examples/wasip3-http-hello-world/src/lib.rs create mode 100644 examples/wasip3-http-send-request/Cargo.toml create mode 100644 examples/wasip3-http-send-request/spin.toml create mode 100644 examples/wasip3-http-send-request/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 959390b..6dae801 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,73 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "sync_wrapper 1.0.2", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "axum-router" +version = "0.1.0" +dependencies = [ + "axum", + "serde", + "spin-sdk", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -162,9 +229,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cap-fs-ext" @@ -281,7 +348,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.12", + "thiserror 2.0.17", ] [[package]] @@ -850,7 +917,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http 1.0.0", + "http 1.3.1", "indexmap", "slab", "tokio", @@ -879,7 +946,7 @@ name = "hello-world" version = "0.1.0" dependencies = [ "anyhow", - "http 1.0.0", + "http 1.3.1", "serde", "spin-sdk", ] @@ -918,9 +985,9 @@ dependencies = [ [[package]] name = "http" -version = "1.0.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -940,24 +1007,24 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.0.0", + "http 1.3.1", ] [[package]] name = "http-body-util" -version = "0.1.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", - "http 1.0.0", - "http-body 1.0.0", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", "pin-project-lite", ] @@ -966,7 +1033,7 @@ name = "http-hello" version = "0.1.0" dependencies = [ "anyhow", - "http 1.0.0", + "http 1.3.1", "spin-sdk", ] @@ -1040,8 +1107,8 @@ dependencies = [ "futures-channel", "futures-util", "h2 0.4.2", - "http 1.0.0", - "http-body 1.0.0", + "http 1.3.1", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1195,7 +1262,7 @@ name = "json-http-rust" version = "0.1.0" dependencies = [ "anyhow", - "http 1.0.0", + "http 1.3.1", "serde", "spin-sdk", ] @@ -1268,6 +1335,12 @@ dependencies = [ "libc", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "maybe-owned" version = "0.3.4" @@ -1720,7 +1793,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", @@ -1761,7 +1834,7 @@ name = "rust-key-value" version = "0.1.0" dependencies = [ "anyhow", - "http 1.0.0", + "http 1.3.1", "spin-sdk", ] @@ -1778,7 +1851,7 @@ name = "rust-outbound-mysql" version = "0.1.0" dependencies = [ "anyhow", - "http 1.0.0", + "http 1.3.1", "serde", "serde_json", "spin-sdk", @@ -1789,7 +1862,7 @@ name = "rust-outbound-pg" version = "0.1.0" dependencies = [ "anyhow", - "http 1.0.0", + "http 1.3.1", "spin-sdk", ] @@ -1799,7 +1872,7 @@ version = "0.1.0" dependencies = [ "anyhow", "chrono", - "http 1.0.0", + "http 1.3.1", "spin-sdk", ] @@ -1808,7 +1881,7 @@ name = "rust-outbound-pg-v4" version = "0.1.0" dependencies = [ "anyhow", - "http 1.0.0", + "http 1.3.1", "spin-sdk", ] @@ -1974,20 +2047,40 @@ dependencies = [ "serde", ] +[[package]] +name = "send-request" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "http 1.3.1", + "spin-sdk", +] + [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2005,6 +2098,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -2057,7 +2161,7 @@ name = "simple-http" version = "0.1.0" dependencies = [ "anyhow", - "http 1.0.0", + "http 1.3.1", "spin-sdk", ] @@ -2148,7 +2252,7 @@ dependencies = [ "chrono", "form_urlencoded", "futures", - "http 1.0.0", + "http 1.3.1", "http-body-util", "hyper 1.2.0", "once_cell", @@ -2160,15 +2264,17 @@ dependencies = [ "serde_json", "spin-executor", "spin-macro", - "thiserror 1.0.57", + "spin-wasip3-http", + "spin-wasip3-http-macro", + "thiserror 2.0.17", "tokio", "uuid", "wasi 0.13.1+wasi-0.2.0", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-http", - "wit-bindgen", - "wit-component", + "wit-bindgen 0.43.0", + "wit-component 0.235.0", ] [[package]] @@ -2191,6 +2297,28 @@ dependencies = [ "url", ] +[[package]] +name = "spin-wasip3-http" +version = "5.0.0" +dependencies = [ + "anyhow", + "bytes", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "wasip3", + "wasip3-http-ext", +] + +[[package]] +name = "spin-wasip3-http-macro" +version = "5.0.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "spinredis" version = "0.1.0" @@ -2257,6 +2385,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + [[package]] name = "system-configuration" version = "0.5.1" @@ -2332,11 +2466,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.12", + "thiserror-impl 2.0.17", ] [[package]] @@ -2352,9 +2486,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -2481,11 +2615,31 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -2664,6 +2818,34 @@ dependencies = [ "url", ] +[[package]] +name = "wasip3" +version = "0.2.0+wasi-0.3.0-rc-2025-09-16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c5ffefc208bc11080d0e6a44e1807cbbd3fc67dafd20078fffb4598421e33" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3-hello-world" +version = "0.1.0" +dependencies = [ + "spin-sdk", +] + +[[package]] +name = "wasip3-http-ext" +version = "5.0.0" +dependencies = [ + "bytes", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "thiserror 2.0.17", + "wasip3", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -2741,7 +2923,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a" dependencies = [ "leb128fmt", - "wasmparser", + "wasmparser 0.235.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser 0.239.0", ] [[package]] @@ -2752,8 +2944,20 @@ checksum = "b055604ba04189d54b8c0ab2c2fc98848f208e103882d5c0b984f045d5ea4d20" dependencies = [ "anyhow", "indexmap", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.235.0", + "wasmparser 0.235.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.239.0", + "wasmparser 0.239.0", ] [[package]] @@ -2769,6 +2973,18 @@ dependencies = [ "serde", ] +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags 2.4.2", + "hashbrown", + "indexmap", + "semver", +] + [[package]] name = "wasmprinter" version = "0.235.0" @@ -2777,7 +2993,7 @@ checksum = "75aa8e9076de6b9544e6dab4badada518cca0bf4966d35b131bbd057aed8fa0a" dependencies = [ "anyhow", "termcolor", - "wasmparser", + "wasmparser 0.235.0", ] [[package]] @@ -2816,8 +3032,8 @@ dependencies = [ "smallvec", "target-lexicon", "trait-variant", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.235.0", + "wasmparser 0.235.0", "wasmtime-environ", "wasmtime-internal-asm-macros", "wasmtime-internal-cache", @@ -2857,8 +3073,8 @@ dependencies = [ "serde_derive", "smallvec", "target-lexicon", - "wasm-encoder", - "wasmparser", + "wasm-encoder 0.235.0", + "wasmparser 0.235.0", "wasmprinter", "wasmtime-internal-component-util", ] @@ -2904,7 +3120,7 @@ dependencies = [ "syn 2.0.104", "wasmtime-internal-component-util", "wasmtime-internal-wit-bindgen", - "wit-parser", + "wit-parser 0.235.0", ] [[package]] @@ -2933,8 +3149,8 @@ dependencies = [ "pulley-interpreter", "smallvec", "target-lexicon", - "thiserror 2.0.12", - "wasmparser", + "thiserror 2.0.17", + "wasmparser 0.235.0", "wasmtime-environ", "wasmtime-internal-math", "wasmtime-internal-versioned-export-macros", @@ -3030,7 +3246,7 @@ dependencies = [ "gimli 0.31.1", "object 0.36.7", "target-lexicon", - "wasmparser", + "wasmparser 0.235.0", "wasmtime-environ", "wasmtime-internal-cranelift", "winch-codegen", @@ -3045,7 +3261,7 @@ dependencies = [ "anyhow", "heck", "indexmap", - "wit-parser", + "wit-parser 0.235.0", ] [[package]] @@ -3069,7 +3285,7 @@ dependencies = [ "io-lifetimes", "rustix 1.0.8", "system-interface", - "thiserror 2.0.12", + "thiserror 2.0.17", "tokio", "tracing", "url", @@ -3089,8 +3305,8 @@ dependencies = [ "async-trait", "bytes", "futures", - "http 1.0.0", - "http-body 1.0.0", + "http 1.3.1", + "http-body 1.0.1", "http-body-util", "hyper 1.2.0", "rustls", @@ -3135,7 +3351,7 @@ dependencies = [ "leb128fmt", "memchr", "unicode-width", - "wasm-encoder", + "wasm-encoder 0.235.0", ] [[package]] @@ -3184,7 +3400,7 @@ dependencies = [ "anyhow", "async-trait", "bitflags 2.4.2", - "thiserror 2.0.12", + "thiserror 2.0.17", "tracing", "wasmtime", "wiggle-macro", @@ -3260,8 +3476,8 @@ dependencies = [ "regalloc2", "smallvec", "target-lexicon", - "thiserror 2.0.12", - "wasmparser", + "thiserror 2.0.17", + "wasmparser 0.235.0", "wasmtime-environ", "wasmtime-internal-cranelift", "wasmtime-internal-math", @@ -3460,7 +3676,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a18712ff1ec5bd09da500fe1e91dec11256b310da0ff33f8b4ec92b927cf0c6" dependencies = [ "wit-bindgen-rt 0.43.0", - "wit-bindgen-rust-macro", + "wit-bindgen-rust-macro 0.43.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags 2.4.2", + "futures", + "once_cell", + "wit-bindgen-rust-macro 0.46.0", ] [[package]] @@ -3471,7 +3699,18 @@ checksum = "2c53468e077362201de11999c85c07c36e12048a990a3e0d69da2bd61da355d0" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.235.0", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.239.0", ] [[package]] @@ -3514,9 +3753,25 @@ dependencies = [ "indexmap", "prettyplease", "syn 2.0.104", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", + "wasm-metadata 0.235.0", + "wit-bindgen-core 0.43.0", + "wit-component 0.235.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.104", + "wasm-metadata 0.239.0", + "wit-bindgen-core 0.46.0", + "wit-component 0.239.0", ] [[package]] @@ -3530,8 +3785,23 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.104", - "wit-bindgen-core", - "wit-bindgen-rust", + "wit-bindgen-core 0.43.0", + "wit-bindgen-rust 0.43.0", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.104", + "wit-bindgen-core 0.46.0", + "wit-bindgen-rust 0.46.0", ] [[package]] @@ -3547,10 +3817,29 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "wasm-encoder 0.235.0", + "wasm-metadata 0.235.0", + "wasmparser 0.235.0", + "wit-parser 0.235.0", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags 2.4.2", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.239.0", + "wasm-metadata 0.239.0", + "wasmparser 0.239.0", + "wit-parser 0.239.0", ] [[package]] @@ -3568,7 +3857,25 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser", + "wasmparser 0.235.0", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.239.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e4e9694..b18c72e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ The Spin Rust SDK makes it easy to build Spin components in Rust. name = "spin_sdk" [dependencies] -anyhow = "1" +anyhow = { workspace = true } async-trait = "0.1.74" chrono = "0.4.38" form_urlencoded = "1.0" @@ -25,14 +25,16 @@ postgres_range = { version = "0.11.1", optional = true } rust_decimal = { version = "1.37.2", default-features = false, optional = true } spin-executor = { version = "5.0.0", path = "crates/executor" } spin-macro = { version = "5.0.0", path = "crates/macro" } -thiserror = "1.0.37" +spin-wasip3-http = { version = "5.0.0", path = "crates/spin-wasip3-http" } +spin-wasip3-http-macro = { version = "5.0.0", path = "crates/spin-wasip3-http-macro" } +thiserror = { workspace = true } uuid = { version = "1.18.0", optional = true } wit-bindgen = { workspace = true } routefinder = "0.5.3" once_cell = { workspace = true } futures = { workspace = true } -bytes = "1" -hyperium = { package = "http", version = "1.0.0" } +bytes = { workspace = true } +hyperium = { workspace = true } serde_json = { version = "1.0.96", optional = true } serde = { version = "1.0.163", optional = true } wasi = { workspace = true } @@ -65,6 +67,9 @@ members = [ "examples/variables", "examples/wasi-http-streaming-outgoing-body", "examples/wasi-http-streaming-file", + "examples/wasip3-http-axum-router", + "examples/wasip3-http-hello-world", + "examples/wasip3-http-send-request", "test-cases/simple-http", "test-cases/simple-redis", "crates/*", @@ -92,12 +97,19 @@ authors = ["Spin Framework Maintainers "] edition = "2021" license = "Apache-2.0 WITH LLVM-exception" repository = "https://github.com/spinframework/spin-rust-sdk" -rust-version = "1.78" +rust-version = "1.86" homepage = "https://spinframework.dev/rust-components" [workspace.dependencies] +anyhow = "1" +hyperium = { package = "http", version = "1.3.1" } +http-body = "1.0.1" +http-body-util = "0.1.3" +bytes = "1.10.1" wit-bindgen = "0.43.0" futures = "0.3.28" once_cell = "1.18.0" +thiserror = "2.0.17" # Pin to the last version that targeted WASI 0.2.0 wasi = "=0.13.1" +wasip3 = "0.2.0" diff --git a/crates/spin-wasip3-http-macro/Cargo.toml b/crates/spin-wasip3-http-macro/Cargo.toml new file mode 100644 index 0000000..0c15e8f --- /dev/null +++ b/crates/spin-wasip3-http-macro/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "spin-wasip3-http-macro" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +homepage.workspace = true +description = """ +Rust procedural macros for Spin and associated WIT files +""" + +[lib] +name = "spin_wasip3_http_macro" +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1.0" +syn = { version = "1.0", features = [ "full" ]} \ No newline at end of file diff --git a/crates/spin-wasip3-http-macro/src/lib.rs b/crates/spin-wasip3-http-macro/src/lib.rs new file mode 100644 index 0000000..8117b47 --- /dev/null +++ b/crates/spin-wasip3-http-macro/src/lib.rs @@ -0,0 +1,37 @@ +use proc_macro::TokenStream; +use quote::quote; + +/// TODO +#[proc_macro_attribute] +pub fn http_component(_attr: TokenStream, item: TokenStream) -> TokenStream { + let func = syn::parse_macro_input!(item as syn::ItemFn); + + if func.sig.asyncness.is_none() { + return syn::Error::new_spanned( + func.sig.fn_token, + "the `#[http_component]` function must be `async`", + ) + .to_compile_error() + .into(); + } + + let func_name = &func.sig.ident; + + quote!( + #func + mod __spin_wasip3_http { + use ::spin_sdk::http_wasip3::IntoResponse; + + struct Spin; + ::spin_sdk::http_wasip3::wasip3::http::proxy::export!(Spin); + + impl ::spin_sdk::http_wasip3::wasip3::exports::http::handler::Guest for self::Spin { + async fn handle(request: ::spin_sdk::http_wasip3::wasip3::http::types::Request) -> Result<::spin_sdk::http_wasip3::wasip3::http::types::Response, ::spin_sdk::http_wasip3::wasip3::http::types::ErrorCode> { + let request = <::spin_sdk::http_wasip3::IncomingRequest as ::spin_sdk::http_wasip3::FromRequest>::from_request(request)?; + ::spin_sdk::http_wasip3::IntoResponse::into_response(super::#func_name(request).await) + } + } + } + ) + .into() +} diff --git a/crates/spin-wasip3-http/Cargo.toml b/crates/spin-wasip3-http/Cargo.toml new file mode 100644 index 0000000..fe20018 --- /dev/null +++ b/crates/spin-wasip3-http/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "spin-wasip3-http" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +homepage.workspace = true + +[dependencies] +anyhow = { workspace = true } +bytes = { workspace = true } +http-body = { workspace = true } +http-body-util = { workspace = true } +hyperium = { workspace = true } +wasip3-http-ext = { version = "5.0.0", path = "../wasip3-http-ext" } +wasip3 = { workspace = true } \ No newline at end of file diff --git a/crates/spin-wasip3-http/src/lib.rs b/crates/spin-wasip3-http/src/lib.rs new file mode 100644 index 0000000..e87f930 --- /dev/null +++ b/crates/spin-wasip3-http/src/lib.rs @@ -0,0 +1,417 @@ +//! Experimental Rust SDK for wasip3 http. + +#![deny(missing_docs)] + +#[doc(hidden)] +pub use wasip3; + +use hyperium as http; +use std::any::Any; +use wasip3::{http::types, wit_bindgen}; +use wasip3_http_ext::body_writer::BodyWriter; +use wasip3_http_ext::helpers::{ + header_map_to_wasi, method_from_wasi, method_to_wasi, scheme_from_wasi, scheme_to_wasi, + to_internal_error_code, +}; +use wasip3_http_ext::RequestOptionsExtension; +use wasip3_http_ext::{IncomingRequestBody, IncomingResponseBody}; + +/// A alias for [`std::result::Result`] that uses [`Error`] as the default error type. +/// +/// This allows functions throughout the crate to return `Result` +/// instead of writing out `Result` explicitly. +pub type Result = ::std::result::Result; + +/// An inbound HTTP request carrying an [`wasip3_http_ext::IncomingRequestBody`]. +/// +/// This type alias specializes [`http::Request`] with the crate’s +/// [`wasip3_http_ext::IncomingRequestBody`] type, representing a request received +/// from the WASI HTTP runtime or an external client. +/// +/// # See also +/// - [`wasip3_http_ext::IncomingRequestBody`]: The body type for inbound HTTP requests. +/// - [`http::Request`]: The standard HTTP request type from the `http` crate. +pub type IncomingRequest = http::Request; + +/// An inbound HTTP response carrying an [`wasip3_http_ext::IncomingResponseBody`]. +/// +/// This type alias specializes [`http::Response`] with the crate’s +/// [`wasip3_http_ext::IncomingResponseBody`] type, representing a response received +/// from the WASI HTTP runtime or a remote endpoint. +/// +/// # See also +/// - [`wasip3_http_ext::IncomingResponseBody`]: The body type for inbound HTTP responses. +/// - [`http::Response`]: The standard HTTP response type from the `http` crate. +pub type IncomingResponse = http::Response; + +type HttpResult = Result; + +/// Sends an HTTP request and returns the corresponding [`wasip3::http::types::Response`]. +/// +/// This function converts the provided value into a [`wasip3::http::types::Request`] using the +/// [`IntoRequest`] trait, dispatches it to the WASI HTTP handler, and awaits +/// the resulting response. It provides a convenient high-level interface for +/// issuing HTTP requests within a WASI environment. +pub async fn send(request: impl IntoRequest) -> HttpResult { + let request = request.into_request()?; + let response = wasip3::http::handler::handle(request).await?; + IncomingResponse::from_response(response) +} + +/// A trait for any type that can be converted into a [`wasip3::http::types::Request`]. +/// +/// This trait provides a unified interface for adapting user-defined request +/// types into the lower-level [`wasip3::http::types::Request`] format used by +/// the WASI HTTP subsystem. +/// +/// Implementing `IntoRequest` allows custom builders or wrapper types to +/// interoperate seamlessly with APIs that expect standardized WASI HTTP +/// request objects. +/// +/// # See also +/// - [`FromRequest`]: The inverse conversion trait. +pub trait IntoRequest { + /// Converts `self` into a [`wasip3::http::types::Request`]. + fn into_request(self) -> HttpResult; +} + +/// A trait for any type that can be converted into a [`wasip3::http::types::Response`]. +/// +/// This trait provides a unified interface for adapting user-defined response +/// types into the lower-level [`wasip3::http::types::Response`] format used by +/// the WASI HTTP subsystem. +/// +/// Implementing `IntoResponse` enables ergonomic conversion from domain-level +/// response types or builders into standardized WASI HTTP responses. +/// +/// # See also +/// - [`FromResponse`]: The inverse conversion trait. +pub trait IntoResponse { + /// Converts `self` into a [`wasip3::http::types::Response`]. + fn into_response(self) -> HttpResult; +} + +/// A trait for constructing a value from a [`wasip3::http::types::Request`]. +/// +/// This is the inverse of [`IntoRequest`], allowing higher-level request +/// types to be built from standardized WASI HTTP requests—for example, +/// to parse structured payloads, extract query parameters, or perform +/// request validation. +/// +/// # See also +/// - [`IntoRequest`]: Converts a type into a [`wasip3::http::types::Request`]. +pub trait FromRequest { + /// Attempts to construct `Self` from a [`wasip3::http::types::Request`]. + fn from_request(req: wasip3::http::types::Request) -> HttpResult + where + Self: Sized; +} + +/// A trait for constructing a value from a [`wasip3::http::types::Response`]. +/// +/// This is the inverse of [`IntoResponse`], allowing higher-level response +/// types to be derived from standardized WASI HTTP responses—for example, +/// to deserialize JSON payloads or map responses to domain-specific types. +/// +/// # See also +/// - [`IntoResponse`]: Converts a type into a [`wasip3::http::types::Response`]. +pub trait FromResponse { + /// Attempts to construct `Self` from a [`wasip3::http::types::Response`]. + fn from_response(response: wasip3::http::types::Response) -> HttpResult + where + Self: Sized; +} + +/// The error type used for HTTP operations within the WASI environment. +/// +/// This enum provides a unified representation of all errors that can occur +/// during HTTP request or response handling, whether they originate from +/// WASI-level error codes, dynamic runtime failures, or full HTTP responses +/// returned as error results. +/// +/// # Variants +/// +/// - [`Error::ErrorCode`]: Wraps a low-level [`wasip3::http::types::ErrorCode`] +/// reported by the WASI HTTP runtime (e.g. connection errors, protocol errors). +/// +/// - [`Error::Other`]: Represents an arbitrary dynamic error implementing +/// [`std::error::Error`]. This allows integration with external libraries or +/// application-specific failure types. +/// +/// - [`Error::Response`]: Contains a full [`wasip3::http::types::Response`] +/// representing an HTTP-level error (for example, a `4xx` or `5xx` response +/// that should be treated as an error condition). +/// +/// # See also +/// - [`wasip3::http::types::ErrorCode`]: Standard WASI HTTP error codes. +/// - [`wasip3::http::types::Response`]: Used when an error represents an HTTP response body. +#[derive(Debug)] +pub enum Error { + /// A low-level WASI HTTP error code. + /// + /// Wraps [`wasip3::http::types::ErrorCode`] to represent + /// transport-level or protocol-level failures. + ErrorCode(wasip3::http::types::ErrorCode), + /// A dynamic application or library error. + /// + /// Used for any runtime error that implements [`std::error::Error`], + /// allowing flexibility for different error sources. + Other(Box), + /// An HTTP response treated as an error. + /// + /// Contains a full [`wasip3::http::types::Response`], such as + /// a `404 Not Found` or `500 Internal Server Error`, when + /// the response itself represents an application-level failure. + Response(wasip3::http::types::Response), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ErrorCode(e) => write!(f, "{e}"), + Error::Other(e) => write!(f, "{e}"), + Error::Response(e) => match http::StatusCode::from_u16(e.get_status_code()) { + Ok(status) => write!(f, "{status}"), + Err(e) => write!(f, "{e}"), + }, + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(_err: http::Error) -> Error { + todo!("map to specific error codes") + } +} + +impl From for Error { + fn from(err: anyhow::Error) -> Error { + match err.downcast::() { + Ok(code) => Error::ErrorCode(code), + Err(other) => match other.downcast::() { + Ok(err) => err, + Err(other) => Error::Other(other.into_boxed_dyn_error()), + }, + } + } +} + +impl From for Error { + fn from(v: std::convert::Infallible) -> Self { + match v {} + } +} + +impl From for Error { + fn from(code: types::ErrorCode) -> Self { + Error::ErrorCode(code) + } +} + +impl From for Error { + fn from(resp: types::Response) -> Self { + Error::Response(resp) + } +} + +impl> IntoResponse for Result { + fn into_response(self) -> HttpResult { + match self { + Ok(ok) => ok.into_response(), + Err(err) => match err.into() { + Error::ErrorCode(code) => Err(code), + Error::Response(resp) => Ok(resp), + Error::Other(other) => { + Err(types::ErrorCode::InternalError(Some(other.to_string()))) + } + }, + } + } +} + +impl IntoRequest for http::Request +where + T: http_body::Body + Any, + T::Data: Into>, + T::Error: Into>, +{ + fn into_request(mut self) -> HttpResult { + if let Some(incoming_body) = + (&mut self as &mut dyn Any).downcast_mut::() + { + if let Some(request) = incoming_body.take_unstarted() { + return Ok(request); + } + } + + let (parts, body) = self.into_parts(); + + let options = parts + .extensions + .get::() + .cloned() + .map(|o| o.0); + + let headers = header_map_to_wasi(&parts.headers)?; + + let (body_writer, contents_rx, trailers_rx) = BodyWriter::new(); + + let (req, _result) = types::Request::new(headers, Some(contents_rx), trailers_rx, options); + + req.set_method(&method_to_wasi(&parts.method)) + .map_err(|()| types::ErrorCode::HttpRequestMethodInvalid)?; + + let scheme = parts.uri.scheme().map(scheme_to_wasi); + req.set_scheme(scheme.as_ref()) + .map_err(|()| types::ErrorCode::HttpProtocolError)?; + + req.set_authority(parts.uri.authority().map(|a| a.as_str())) + .map_err(|()| types::ErrorCode::HttpRequestUriInvalid)?; + + req.set_path_with_query(parts.uri.path_and_query().map(|pq| pq.as_str())) + .map_err(|()| types::ErrorCode::HttpRequestUriInvalid)?; + + wit_bindgen::spawn(async move { + let mut body = std::pin::pin!(body); + _ = body_writer.forward_http_body(&mut body).await; + }); + + Ok(req) + } +} + +impl FromRequest for types::Request { + fn from_request(req: types::Request) -> HttpResult { + Ok(req) + } +} + +impl FromRequest for http::Request { + fn from_request(req: types::Request) -> HttpResult { + let uri = { + let mut builder = http::Uri::builder(); + if let Some(scheme) = req.get_scheme() { + builder = builder.scheme(scheme_from_wasi(scheme)?); + } + if let Some(authority) = req.get_authority() { + builder = builder.authority(authority); + } + if let Some(path_and_query) = req.get_path_with_query() { + builder = builder.path_and_query(path_and_query); + } + builder + .build() + .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)? + }; + + let mut builder = http::Request::builder() + .method(method_from_wasi(req.get_method())?) + .uri(uri); + + if let Some(options) = req.get_options().map(RequestOptionsExtension) { + builder = builder.extension(options); + } + + for (k, v) in req.get_headers().copy_all() { + builder = builder.header(k, v); + } + + let body = T::from_request(req)?; + + builder.body(body).map_err(to_internal_error_code) // TODO: downcast to more specific http error codes + } +} + +impl FromResponse for http::Response { + fn from_response(resp: types::Response) -> HttpResult { + let mut builder = http::Response::builder().status(resp.get_status_code()); + + for (k, v) in resp.get_headers().copy_all() { + builder = builder.header(k, v); + } + + let body = T::from_response(resp)?; + builder.body(body).map_err(to_internal_error_code) // TODO: downcast to more specific http error codes + } +} + +impl FromRequest for () { + fn from_request(_req: types::Request) -> HttpResult { + Ok(()) + } +} + +impl IntoResponse for types::Response { + fn into_response(self) -> HttpResult { + Ok(self) + } +} + +impl IntoResponse for (http::StatusCode, T) { + fn into_response(self) -> HttpResult { + unreachable!() + } +} + +impl IntoResponse for &'static str { + fn into_response(self) -> HttpResult { + http::Response::new(http_body_util::Full::new(self.as_bytes())).into_response() + } +} + +impl IntoResponse for String { + fn into_response(self) -> HttpResult { + http::Response::new(self).into_response() + } +} + +impl IntoResponse for http::Response +where + T: http_body::Body + Any, + T::Data: Into>, + T::Error: Into>, +{ + fn into_response(mut self) -> HttpResult { + if let Some(incoming_body) = + (&mut self as &mut dyn Any).downcast_mut::() + { + if let Some(response) = incoming_body.take_unstarted() { + return Ok(response); + } + } + + let headers = header_map_to_wasi(self.headers())?; + + let (body_writer, body_rx, body_result_rx) = BodyWriter::new(); + + let (response, _future_result) = + types::Response::new(headers, Some(body_rx), body_result_rx); + + wit_bindgen::spawn(async move { + let mut body = std::pin::pin!(self.into_body()); + _ = body_writer.forward_http_body(&mut body).await; + }); + + Ok(response) + } +} + +impl FromRequest for IncomingRequestBody { + fn from_request(req: types::Request) -> HttpResult + where + Self: Sized, + { + Self::new(req) + } +} + +impl FromResponse for IncomingResponseBody { + fn from_response(response: types::Response) -> HttpResult + where + Self: Sized, + { + Self::new(response) + } +} diff --git a/crates/wasip3-http-ext/Cargo.toml b/crates/wasip3-http-ext/Cargo.toml new file mode 100644 index 0000000..4ffecfe --- /dev/null +++ b/crates/wasip3-http-ext/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wasip3-http-ext" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +homepage.workspace = true + +[dependencies] +bytes = { workspace = true } +http-body = { workspace = true } +http-body-util = { workspace = true } +hyperium = { workspace = true } +wasip3 = { workspace = true } +thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/wasip3-http-ext/src/body_writer.rs b/crates/wasip3-http-ext/src/body_writer.rs new file mode 100644 index 0000000..175dec0 --- /dev/null +++ b/crates/wasip3-http-ext/src/body_writer.rs @@ -0,0 +1,144 @@ +use std::fmt::Debug; + +use http_body::Frame; +use http_body_util::BodyExt as _; +use hyperium::HeaderMap; +use wasip3::{ + http::types::{ErrorCode, HeaderError, Trailers}, + wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter}, + wit_future, wit_stream, +}; + +use crate::helpers::header_map_to_fields; + +type BoxError = Box; + +pub type BodyResult = Result, ErrorCode>; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The [`http_body::Body`] returned an error. + #[error("body error: {0}")] + HttpBody(#[source] BoxError), + + /// Received trailers were rejected by [`Trailers::from_list`]. + #[error("invalid trailers: {0}")] + InvalidTrailers(#[source] HeaderError), + + /// The result future reader end was closed (dropped). + /// + /// The result that couldn't be written is returned. + #[error("result future reader closed")] + ResultReaderClosed(BodyResult), + + /// The stream reader end was closed (dropped). + /// + /// The number of bytes written successfully is returned as `written` and + /// the bytes that couldn't be written are returned as `unwritten`. + #[error("stream reader closed")] + StreamReaderClosed { written: usize, unwritten: Vec }, +} + +/// BodyWriter coordinates a [`StreamWriter`] and [`FutureWriter`] associated +/// with the write end of a `wasi:http` `Request` or `Response` body. +pub struct BodyWriter { + pub stream_writer: StreamWriter, + pub result_writer: FutureWriter, + pub trailers: HeaderMap, +} + +impl BodyWriter { + /// Returns a new writer and the matching stream and result future readers, + /// which will typically be used to create a `wasi:http` `Request` or + /// `Response`. + pub fn new() -> (Self, StreamReader, FutureReader) { + let (stream_writer, stream_reader) = wit_stream::new(); + let (result_writer, result_reader) = + // TODO: is there a more appropriate ErrorCode? + wit_future::new(|| Err(ErrorCode::InternalError(Some("body writer dropped".into())))); + ( + Self { + stream_writer, + result_writer, + trailers: Default::default(), + }, + stream_reader, + result_reader, + ) + } + + /// Forwards the given [`http_body::Body`] to this writer. + /// + /// This copies all data frames from the body to this writer's stream and + /// then writes any trailers from the body to the result future. On success + /// the number of data bytes written to the stream (which does not including + /// trailers) is returned. + /// + /// If there is an error it is written to to the result future. + pub async fn forward_http_body(mut self, body: &mut T) -> Result + where + T: http_body::Body + Unpin, + T::Data: Into>, + T::Error: Into, + { + let mut total_written = 0; + loop { + match body.frame().await { + Some(Ok(frame)) => { + let written = self.process_http_body_frame(frame).await?; + total_written += written as u64; + } + Some(Err(err)) => { + let err = err.into(); + // TODO: consider if there are better ErrorCode mappings + let error_code = ErrorCode::InternalError(Some(err.to_string())); + // TODO: log result_writer.write errors? + _ = self.result_writer.write(Err(error_code)).await; + return Err(Error::HttpBody(err)); + } + None => break, + } + } + let maybe_trailers = if self.trailers.is_empty() { + None + } else { + Some(header_map_to_fields(self.trailers).map_err(Error::InvalidTrailers)?) + }; + match self.result_writer.write(Ok(maybe_trailers)).await { + Ok(()) => Ok(total_written), + Err(err) => Err(Error::ResultReaderClosed(err.value)), + } + } + + /// Processes a [`http_body::Frame`]. + /// + /// - If the frame contains data, the data is written to this writer's + /// stream and the size of the written data is returned. + /// - If the frame contains trailers they are added to [`Self::trailers`] + /// and `Ok(0)` is returned. + pub async fn process_http_body_frame(&mut self, frame: Frame) -> Result + where + T: Into>, + { + // Frame is a pseudo-enum which is either 'data' or 'trailers' + if frame.is_data() { + let data = frame.into_data().unwrap_or_else(|_| unreachable!()).into(); + let data_len = data.len(); + // write_all returns any unwritten data if the read end is dropped + let unwritten = self.stream_writer.write_all(data).await; + if !unwritten.is_empty() { + return Err(Error::StreamReaderClosed { + written: data_len - unwritten.len(), + unwritten, + }); + } + Ok(data_len) + } else if frame.is_trailers() { + let trailers = frame.into_trailers().unwrap_or_else(|_| unreachable!()); + self.trailers.extend(trailers); + Ok(0) + } else { + unreachable!("Frames are data or trailers"); + } + } +} diff --git a/crates/wasip3-http-ext/src/lib.rs b/crates/wasip3-http-ext/src/lib.rs new file mode 100644 index 0000000..2928214 --- /dev/null +++ b/crates/wasip3-http-ext/src/lib.rs @@ -0,0 +1,365 @@ +//! Extension types for wasip3::http + +pub mod body_writer; + +use bytes::Bytes; +use helpers::{fields_to_header_map, get_content_length, to_internal_error_code}; +use http_body::SizeHint; +use http_body_util::{BodyExt, BodyStream}; +use hyperium as http; +use std::{ + pin::Pin, + task::{self, Poll}, +}; +use wasip3::{ + http::types::{self, ErrorCode}, + wit_bindgen::{self, StreamResult}, + wit_future, +}; + +const READ_FRAME_SIZE: usize = 16 * 1024; + +pub type IncomingRequestBody = IncomingBody; +pub type IncomingResponseBody = IncomingBody; + +pub struct RequestOptionsExtension(pub types::RequestOptions); + +impl Clone for RequestOptionsExtension { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +pub trait IncomingMessage: Unpin { + fn get_headers(&self) -> types::Headers; + + fn consume_body( + self, + res: wit_bindgen::FutureReader>, + ) -> ( + wit_bindgen::StreamReader, + wit_bindgen::FutureReader, ErrorCode>>, + ); +} + +impl IncomingMessage for types::Request { + fn get_headers(&self) -> types::Headers { + self.get_headers() + } + + fn consume_body( + self, + res: wit_bindgen::FutureReader>, + ) -> ( + wit_bindgen::StreamReader, + wit_bindgen::FutureReader, ErrorCode>>, + ) { + Self::consume_body(self, res) + } +} + +impl IncomingMessage for types::Response { + fn get_headers(&self) -> types::Headers { + self.get_headers() + } + + fn consume_body( + self, + res: wit_bindgen::FutureReader>, + ) -> ( + wit_bindgen::StreamReader, + wit_bindgen::FutureReader, ErrorCode>>, + ) { + Self::consume_body(self, res) + } +} + +/// A stream of Bytes, used when receiving bodies from the network. +pub struct IncomingBody { + state: StartedState, + content_length: Option, +} + +enum StartedState { + Unstarted(T), + Started { + #[allow(dead_code)] + result: wit_bindgen::FutureWriter>, + state: IncomingState, + }, + Empty, +} + +impl IncomingBody { + pub fn new(msg: T) -> Result { + let content_length = get_content_length(msg.get_headers())?; + Ok(Self { + state: StartedState::Unstarted(msg), + content_length, + }) + } + + pub async fn stream(self) -> BodyStream { + BodyStream::new(self) + } + + pub async fn bytes(self) -> Result { + self.collect().await.map(|c| c.to_bytes()) + } + + // TODO: pub fn take_future() -> result + + pub fn take_unstarted(&mut self) -> Option { + match self.state { + StartedState::Unstarted(_) => { + let StartedState::Unstarted(msg) = + std::mem::replace(&mut self.state, StartedState::Empty) + else { + unreachable!(); + }; + Some(msg) + } + _ => None, + } + } + + fn ensure_started(&mut self) -> Result<&mut IncomingState, ErrorCode> { + if let StartedState::Unstarted(_) = self.state { + let msg = self.take_unstarted().unwrap(); + let (result, reader) = wit_future::new(|| Ok(())); + let (stream, trailers) = msg.consume_body(reader); + self.state = StartedState::Started { + result, + state: IncomingState::Ready { stream, trailers }, + }; + }; + match &mut self.state { + StartedState::Started { state, .. } => Ok(state), + StartedState::Unstarted(_) => unreachable!(), + StartedState::Empty => Err(to_internal_error_code( + "cannot use IncomingBody after call to take_unstarted", + )), + } + } +} + +enum IncomingState { + Ready { + stream: wit_bindgen::StreamReader, + trailers: wit_bindgen::FutureReader, ErrorCode>>, + }, + Reading(Pin + 'static + Send>>), + Done, +} + +enum ReadResult { + Trailers(Result, ErrorCode>), + BodyChunk { + chunk: Vec, + stream: wit_bindgen::StreamReader, + trailers: wit_bindgen::FutureReader, ErrorCode>>, + }, +} + +impl http_body::Body for IncomingBody { + type Data = Bytes; + type Error = ErrorCode; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> Poll, Self::Error>>> { + let state = self.ensure_started()?; + loop { + match state { + IncomingState::Ready { .. } => { + let IncomingState::Ready { + mut stream, + trailers, + } = std::mem::replace(state, IncomingState::Done) + else { + unreachable!(); + }; + *state = IncomingState::Reading(Box::pin(async move { + let (result, chunk) = + stream.read(Vec::with_capacity(READ_FRAME_SIZE)).await; + match result { + StreamResult::Complete(_n) => ReadResult::BodyChunk { + chunk, + stream, + trailers, + }, + StreamResult::Cancelled => unreachable!(), + StreamResult::Dropped => ReadResult::Trailers(trailers.await), + } + })); + } + IncomingState::Reading(future) => { + match std::task::ready!(future.as_mut().poll(cx)) { + ReadResult::BodyChunk { + chunk, + stream, + trailers, + } => { + *state = IncomingState::Ready { stream, trailers }; + break Poll::Ready(Some(Ok(http_body::Frame::data(chunk.into())))); + } + ReadResult::Trailers(trailers) => { + *state = IncomingState::Done; + match trailers { + Ok(Some(fields)) => { + let trailers = fields_to_header_map(fields)?; + break Poll::Ready(Some(Ok(http_body::Frame::trailers( + trailers, + )))); + } + Ok(None) => {} + Err(e) => { + break Poll::Ready(Some(Err(e))); + } + } + } + } + } + IncomingState::Done => break Poll::Ready(None), + } + } + } + + fn is_end_stream(&self) -> bool { + matches!( + self.state, + StartedState::Started { + state: IncomingState::Done, + .. + } + ) + } + + fn size_hint(&self) -> SizeHint { + let Some(n) = self.content_length else { + return SizeHint::default(); + }; + let mut size_hint = SizeHint::new(); + size_hint.set_lower(0); + size_hint.set_upper(n); + size_hint + } +} + +pub mod helpers { + use super::*; + + pub fn get_content_length(headers: types::Headers) -> Result, ErrorCode> { + let values = headers.get(http::header::CONTENT_LENGTH.as_str()); + if values.len() > 1 { + return Err(to_internal_error_code("multiple content-length values")); + } + let Some(value_bytes) = values.into_iter().next() else { + return Ok(None); + }; + let value_str = std::str::from_utf8(&value_bytes).map_err(to_internal_error_code)?; + let value_i64: i64 = value_str.parse().map_err(to_internal_error_code)?; + let value = value_i64.try_into().map_err(to_internal_error_code)?; + Ok(Some(value)) + } + + pub fn fields_to_header_map(headers: types::Headers) -> Result { + headers + .copy_all() + .into_iter() + .try_fold(http::HeaderMap::new(), |mut map, (k, v)| { + let v = http::HeaderValue::from_bytes(&v).map_err(to_internal_error_code)?; + let k: http::HeaderName = k.parse().map_err(to_internal_error_code)?; + map.append(k, v); + Ok(map) + }) + } + + pub fn scheme_from_wasi(scheme: types::Scheme) -> Result { + match scheme { + types::Scheme::Http => Ok(http::uri::Scheme::HTTP), + types::Scheme::Https => Ok(http::uri::Scheme::HTTPS), + types::Scheme::Other(s) => s + .parse() + .map_err(|_| types::ErrorCode::HttpRequestUriInvalid), + } + } + + pub fn scheme_to_wasi(scheme: &http::uri::Scheme) -> types::Scheme { + match scheme { + s if s == &http::uri::Scheme::HTTP => types::Scheme::Http, + s if s == &http::uri::Scheme::HTTPS => types::Scheme::Https, + other => types::Scheme::Other(other.to_string()), + } + } + + pub fn method_from_wasi(method: types::Method) -> Result { + match method { + types::Method::Get => Ok(http::Method::GET), + types::Method::Post => Ok(http::Method::POST), + types::Method::Put => Ok(http::Method::PUT), + types::Method::Delete => Ok(http::Method::DELETE), + types::Method::Patch => Ok(http::Method::PATCH), + types::Method::Head => Ok(http::Method::HEAD), + types::Method::Options => Ok(http::Method::OPTIONS), + types::Method::Connect => Ok(http::Method::CONNECT), + types::Method::Trace => Ok(http::Method::TRACE), + types::Method::Other(o) => http::Method::from_bytes(o.as_bytes()) + .map_err(|_| types::ErrorCode::HttpRequestMethodInvalid), + } + } + + pub fn method_to_wasi(method: &http::Method) -> types::Method { + match method { + &http::Method::GET => types::Method::Get, + &http::Method::POST => types::Method::Post, + &http::Method::PUT => types::Method::Put, + &http::Method::DELETE => types::Method::Delete, + &http::Method::PATCH => types::Method::Patch, + &http::Method::HEAD => types::Method::Head, + &http::Method::OPTIONS => types::Method::Options, + &http::Method::CONNECT => types::Method::Connect, + &http::Method::TRACE => types::Method::Trace, + other => types::Method::Other(other.to_string()), + } + } + + pub fn header_map_to_wasi(map: &http::HeaderMap) -> Result { + types::Fields::from_list( + &map.iter() + .map(|(k, v)| (k.to_string(), v.as_ref().to_vec())) + .collect::>(), + ) + .map_err(to_internal_error_code) + } + + pub fn header_map_to_field_entries( + map: http::HeaderMap, + ) -> impl Iterator)> { + // https://docs.rs/http/1.3.1/http/header/struct.HeaderMap.html#method.into_iter-2 + // For each yielded item that has None provided for the HeaderName, then + // the associated header name is the same as that of the previously + // yielded item. The first yielded item will have HeaderName set. + let mut last_name = None; + map.into_iter().map(move |(name, value)| { + if name.is_some() { + last_name = name; + } + let name = last_name + .as_ref() + .expect("HeaderMap::into_iter always returns Some(name) before None"); + let value = bytes::Bytes::from_owner(value).to_vec(); + (name.as_str().into(), value) + }) + } + + pub fn header_map_to_fields(map: http::HeaderMap) -> Result { + let entries = Vec::from_iter(header_map_to_field_entries(map)); + types::Fields::from_list(&entries) + } + + pub fn to_internal_error_code(e: impl ::std::fmt::Display) -> ErrorCode { + ErrorCode::InternalError(Some(e.to_string())) + } +} diff --git a/examples/wasip3-http-axum-router/.gitignore b/examples/wasip3-http-axum-router/.gitignore new file mode 100644 index 0000000..386474f --- /dev/null +++ b/examples/wasip3-http-axum-router/.gitignore @@ -0,0 +1,2 @@ +target/ +.spin/ diff --git a/examples/wasip3-http-axum-router/Cargo.toml b/examples/wasip3-http-axum-router/Cargo.toml new file mode 100644 index 0000000..6694d9e --- /dev/null +++ b/examples/wasip3-http-axum-router/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "axum-router" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +axum = { version = "0.8.1", default-features = false, features = ["json", "macros"] } +serde = { version = "1.0.163", features = ["derive"] } +spin-sdk = { path = "../.." } +tower-service = "0.3.3" \ No newline at end of file diff --git a/examples/wasip3-http-axum-router/README.md b/examples/wasip3-http-axum-router/README.md new file mode 100644 index 0000000..4477ac5 --- /dev/null +++ b/examples/wasip3-http-axum-router/README.md @@ -0,0 +1,9 @@ +# axum-router + +This example shows how to use [axum](https://github.com/tokio-rs/axum) with the `spin-sdk` + +``` +spin up --build +curl --json '{"username": "jiggs"}' localhost:3000/users +{"id":1337,"username":"jiggs"} +``` \ No newline at end of file diff --git a/examples/wasip3-http-axum-router/spin.toml b/examples/wasip3-http-axum-router/spin.toml new file mode 100644 index 0000000..94f9af9 --- /dev/null +++ b/examples/wasip3-http-axum-router/spin.toml @@ -0,0 +1,20 @@ +#:schema https://schemas.spinframework.dev/spin/manifest-v2/latest.json + +spin_manifest_version = 2 + +[application] +name = "axum-router" +version = "0.1.0" +authors = ["Fermyon Engineering "] +description = "An example application using axum" + +[[trigger.http]] +route = "/..." +component = "axum-router" + +[component.axum-router] +source = "../../target/wasm32-wasip1/release/axum_router.wasm" +allowed_outbound_hosts = [] +[component.axum-router.build] +command = "cargo build --target wasm32-wasip1 --release" +watch = ["src/**/*.rs", "Cargo.toml"] diff --git a/examples/wasip3-http-axum-router/src/lib.rs b/examples/wasip3-http-axum-router/src/lib.rs new file mode 100644 index 0000000..5a9db9b --- /dev/null +++ b/examples/wasip3-http-axum-router/src/lib.rs @@ -0,0 +1,51 @@ +use axum::{ + http::StatusCode, + routing::{get, post}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; +use spin_sdk::http_wasip3::{http_component, IncomingRequest, IntoResponse}; +use tower_service::Service; + +/// Sends a request to a URL. +#[http_component] +async fn handler(req: IncomingRequest) -> impl IntoResponse { + Router::new() + .route("/", get(root)) + .route("/users", post(create_user)) + .call(req) + .await +} + +async fn root() -> &'static str { + "hello, world!" +} + +async fn create_user( + // this argument tells axum to parse the request body + // as JSON into a `CreateUser` type + Json(payload): Json, +) -> (StatusCode, Json) { + // insert your application logic here + let user = User { + id: 1337, + username: payload.username, + }; + + // this will be converted into a JSON response + // with a status code of `201 Created` + (StatusCode::CREATED, Json(user)) +} + +// the input to our `create_user` handler +#[derive(Deserialize)] +struct CreateUser { + username: String, +} + +// the output to our `create_user` handler +#[derive(Serialize)] +struct User { + id: u64, + username: String, +} diff --git a/examples/wasip3-http-hello-world/Cargo.toml b/examples/wasip3-http-hello-world/Cargo.toml new file mode 100644 index 0000000..b67ccbc --- /dev/null +++ b/examples/wasip3-http-hello-world/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wasip3-hello-world" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +spin-sdk = { path = "../.." } \ No newline at end of file diff --git a/examples/wasip3-http-hello-world/spin.toml b/examples/wasip3-http-hello-world/spin.toml new file mode 100644 index 0000000..30ced24 --- /dev/null +++ b/examples/wasip3-http-hello-world/spin.toml @@ -0,0 +1,20 @@ +#:schema https://schemas.spinframework.dev/spin/manifest-v2/latest.json + +spin_manifest_version = 2 + +[application] +authors = ["Fermyon Engineering "] +description = "An application that returns hello." +name = "hello-world" +version = "1.0.0" + +[[trigger.http]] +route = "/hello" +component = "hello" + +[component.hello] +source = "../../target/wasm32-wasip1/release/wasip3_hello_world.wasm" +description = "A component that returns hello." +[component.hello.build] +command = "cargo build --target wasm32-wasip1 --release" +watch = ["src/**/*.rs", "Cargo.toml"] \ No newline at end of file diff --git a/examples/wasip3-http-hello-world/src/lib.rs b/examples/wasip3-http-hello-world/src/lib.rs new file mode 100644 index 0000000..760619b --- /dev/null +++ b/examples/wasip3-http-hello-world/src/lib.rs @@ -0,0 +1,7 @@ +use spin_sdk::http_wasip3::{http_component, IncomingRequest}; + +/// A simple Spin HTTP component. +#[http_component] +async fn hello_world(_req: IncomingRequest) -> &'static str { + "Hello, world!" +} diff --git a/examples/wasip3-http-send-request/Cargo.toml b/examples/wasip3-http-send-request/Cargo.toml new file mode 100644 index 0000000..83aa3f2 --- /dev/null +++ b/examples/wasip3-http-send-request/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "send-request" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +axum = { version = "0.8.1", default-features = false, features = ["macros"] } +http = "1.3.1" +spin-sdk = { path = "../.." } \ No newline at end of file diff --git a/examples/wasip3-http-send-request/spin.toml b/examples/wasip3-http-send-request/spin.toml new file mode 100644 index 0000000..bbf944e --- /dev/null +++ b/examples/wasip3-http-send-request/spin.toml @@ -0,0 +1,23 @@ +#:schema https://schemas.spinframework.dev/spin/manifest-v2/latest.json + +spin_manifest_version = 2 + +[application] +authors = ["Fermyon Engineering "] +description = "An application that sends an HTTP request" +name = "send-request" +version = "1.0.0" + +[[trigger.http]] +route = "/..." +component = "send" + +[component.send] +source = "../../target/wasm32-wasip1/release/send_request.wasm" +description = "A component that sends a request." +allowed_outbound_hosts = [ + "https://bytecodealliance.org", +] +[component.send.build] +command = "cargo build --target wasm32-wasip1 --release" +watch = ["src/**/*.rs", "Cargo.toml"] \ No newline at end of file diff --git a/examples/wasip3-http-send-request/src/lib.rs b/examples/wasip3-http-send-request/src/lib.rs new file mode 100644 index 0000000..3ad58dd --- /dev/null +++ b/examples/wasip3-http-send-request/src/lib.rs @@ -0,0 +1,10 @@ +use axum::body::Body; +use spin_sdk::http_wasip3::{http_component, send, IncomingRequest, IntoResponse, Result}; + +/// Sends a request to a URL. +#[http_component] +async fn send_request(_req: IncomingRequest) -> Result { + let outgoing = http::Request::get("https://bytecodealliance.org").body(Body::empty())?; + + Ok(send(outgoing).await?) +} diff --git a/src/lib.rs b/src/lib.rs index 53e115a..da1846a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,15 @@ pub mod llm; pub use spin_macro::*; +/// WasiP3 HTTP APIs and helpers +pub mod http_wasip3 { + /// Re-exports the helpers types for converting between WasiP3 HTTP types and + /// Rust ecosystem HTTP types. + pub use spin_wasip3_http::*; + /// Re-exports the macro to enable WasiP3 HTTP handlers + pub use spin_wasip3_http_macro::*; +} + #[doc(hidden)] /// Module containing wit bindgen generated code. /// From ddd7b29310d9d4b90fdca4fcf1e94c57d360a9a6 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Thu, 16 Oct 2025 14:23:23 -0600 Subject: [PATCH 2/5] Address PR comments Signed-off-by: Brian Hardock --- Cargo.lock | 4 +- Cargo.toml | 6 +- crates/spin-wasip3-http-macro/Cargo.toml | 2 +- crates/spin-wasip3-http-macro/src/lib.rs | 30 +- crates/spin-wasip3-http/Cargo.toml | 3 +- crates/spin-wasip3-http/src/lib.rs | 309 +++++++++++++------ crates/wasip3-http-ext/Cargo.toml | 3 +- crates/wasip3-http-ext/src/body_writer.rs | 14 +- crates/wasip3-http-ext/src/lib.rs | 13 +- examples/wasip3-http-axum-router/Cargo.toml | 2 +- examples/wasip3-http-axum-router/spin.toml | 4 +- examples/wasip3-http-axum-router/src/lib.rs | 4 +- examples/wasip3-http-hello-world/Cargo.toml | 2 +- examples/wasip3-http-hello-world/spin.toml | 4 +- examples/wasip3-http-hello-world/src/lib.rs | 4 +- examples/wasip3-http-send-request/Cargo.toml | 2 +- examples/wasip3-http-send-request/spin.toml | 4 +- examples/wasip3-http-send-request/src/lib.rs | 7 +- src/lib.rs | 7 +- 19 files changed, 274 insertions(+), 150 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6dae801..713b986 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -2306,7 +2306,6 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "wasip3", "wasip3-http-ext", ] @@ -2841,7 +2840,6 @@ dependencies = [ "bytes", "http 1.3.1", "http-body 1.0.1", - "http-body-util", "thiserror 2.0.17", "wasip3", ] diff --git a/Cargo.toml b/Cargo.toml index b18c72e..6ebca34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,8 @@ postgres_range = { version = "0.11.1", optional = true } rust_decimal = { version = "1.37.2", default-features = false, optional = true } spin-executor = { version = "5.0.0", path = "crates/executor" } spin-macro = { version = "5.0.0", path = "crates/macro" } -spin-wasip3-http = { version = "5.0.0", path = "crates/spin-wasip3-http" } -spin-wasip3-http-macro = { version = "5.0.0", path = "crates/spin-wasip3-http-macro" } +spin-wasip3-http = { version = "5.0.0", path = "crates/spin-wasip3-http", optional = true } +spin-wasip3-http-macro = { version = "5.0.0", path = "crates/spin-wasip3-http-macro", optional = true } thiserror = { workspace = true } uuid = { version = "1.18.0", optional = true } wit-bindgen = { workspace = true } @@ -44,6 +44,7 @@ default = ["export-sdk-language", "json", "postgres4-types"] export-sdk-language = [] json = ["dep:serde", "dep:serde_json"] postgres4-types = ["dep:rust_decimal", "dep:uuid", "dep:postgres_range", "json"] +wasip3-unstable = ["dep:spin-wasip3-http", "dep:spin-wasip3-http-macro"] [workspace] resolver = "2" @@ -112,4 +113,3 @@ once_cell = "1.18.0" thiserror = "2.0.17" # Pin to the last version that targeted WASI 0.2.0 wasi = "=0.13.1" -wasip3 = "0.2.0" diff --git a/crates/spin-wasip3-http-macro/Cargo.toml b/crates/spin-wasip3-http-macro/Cargo.toml index 0c15e8f..9ebc492 100644 --- a/crates/spin-wasip3-http-macro/Cargo.toml +++ b/crates/spin-wasip3-http-macro/Cargo.toml @@ -8,7 +8,7 @@ repository.workspace = true rust-version.workspace = true homepage.workspace = true description = """ -Rust procedural macros for Spin and associated WIT files +Rust procedural macros for Spin and WASIp3 """ [lib] diff --git a/crates/spin-wasip3-http-macro/src/lib.rs b/crates/spin-wasip3-http-macro/src/lib.rs index 8117b47..56e32eb 100644 --- a/crates/spin-wasip3-http-macro/src/lib.rs +++ b/crates/spin-wasip3-http-macro/src/lib.rs @@ -1,7 +1,33 @@ use proc_macro::TokenStream; use quote::quote; -/// TODO +/// Marks an `async fn` as an HTTP component entrypoint for Spin. +/// +/// The `#[http_component]` attribute designates an asynchronous function as the +/// handler for incoming HTTP requests in a Spin component using the WASI Preview 3 +/// (`wasip3`) HTTP ABI. +/// +/// When applied, this macro generates the necessary boilerplate to export the +/// function to the Spin runtime as a valid HTTP handler. The function must be +/// declared `async` and take a single argument implementing +/// [`FromRequest`](::spin_sdk::http_wasip3::FromRequest), typically +/// [`Request`](::spin_sdk::http_wasip3::Request), and must return a type that +/// implements [`IntoResponse`](::spin_sdk::http_wasip3::IntoResponse). +/// +/// # Requirements +/// +/// - The annotated function **must** be `async`. +/// - The function’s parameter type must implement [`FromRequest`]. +/// - The return type must implement [`IntoResponse`]. +/// +/// If the function is not asynchronous, the macro emits a compile-time error. +/// +/// # Generated Code +/// +/// The macro expands into a module containing a `Spin` struct that implements the +/// WASI `http.handler/Guest` interface, wiring the annotated function as the +/// handler’s entrypoint. This allows the function to be invoked automatically +/// by the Spin runtime when HTTP requests are received. #[proc_macro_attribute] pub fn http_component(_attr: TokenStream, item: TokenStream) -> TokenStream { let func = syn::parse_macro_input!(item as syn::ItemFn); @@ -27,7 +53,7 @@ pub fn http_component(_attr: TokenStream, item: TokenStream) -> TokenStream { impl ::spin_sdk::http_wasip3::wasip3::exports::http::handler::Guest for self::Spin { async fn handle(request: ::spin_sdk::http_wasip3::wasip3::http::types::Request) -> Result<::spin_sdk::http_wasip3::wasip3::http::types::Response, ::spin_sdk::http_wasip3::wasip3::http::types::ErrorCode> { - let request = <::spin_sdk::http_wasip3::IncomingRequest as ::spin_sdk::http_wasip3::FromRequest>::from_request(request)?; + let request = <::spin_sdk::http_wasip3::Request as ::spin_sdk::http_wasip3::FromRequest>::from_request(request)?; ::spin_sdk::http_wasip3::IntoResponse::into_response(super::#func_name(request).await) } } diff --git a/crates/spin-wasip3-http/Cargo.toml b/crates/spin-wasip3-http/Cargo.toml index fe20018..6b3a625 100644 --- a/crates/spin-wasip3-http/Cargo.toml +++ b/crates/spin-wasip3-http/Cargo.toml @@ -14,5 +14,4 @@ bytes = { workspace = true } http-body = { workspace = true } http-body-util = { workspace = true } hyperium = { workspace = true } -wasip3-http-ext = { version = "5.0.0", path = "../wasip3-http-ext" } -wasip3 = { workspace = true } \ No newline at end of file +wasip3-http-ext = { version = "5.0.0", path = "../wasip3-http-ext" } \ No newline at end of file diff --git a/crates/spin-wasip3-http/src/lib.rs b/crates/spin-wasip3-http/src/lib.rs index e87f930..ac95e4c 100644 --- a/crates/spin-wasip3-http/src/lib.rs +++ b/crates/spin-wasip3-http/src/lib.rs @@ -1,9 +1,9 @@ -//! Experimental Rust SDK for wasip3 http. +//! Experimental Rust SDK for WASIp3 http. #![deny(missing_docs)] #[doc(hidden)] -pub use wasip3; +pub use wasip3_http_ext::wasip3; use hyperium as http; use std::any::Any; @@ -22,106 +22,8 @@ use wasip3_http_ext::{IncomingRequestBody, IncomingResponseBody}; /// instead of writing out `Result` explicitly. pub type Result = ::std::result::Result; -/// An inbound HTTP request carrying an [`wasip3_http_ext::IncomingRequestBody`]. -/// -/// This type alias specializes [`http::Request`] with the crate’s -/// [`wasip3_http_ext::IncomingRequestBody`] type, representing a request received -/// from the WASI HTTP runtime or an external client. -/// -/// # See also -/// - [`wasip3_http_ext::IncomingRequestBody`]: The body type for inbound HTTP requests. -/// - [`http::Request`]: The standard HTTP request type from the `http` crate. -pub type IncomingRequest = http::Request; - -/// An inbound HTTP response carrying an [`wasip3_http_ext::IncomingResponseBody`]. -/// -/// This type alias specializes [`http::Response`] with the crate’s -/// [`wasip3_http_ext::IncomingResponseBody`] type, representing a response received -/// from the WASI HTTP runtime or a remote endpoint. -/// -/// # See also -/// - [`wasip3_http_ext::IncomingResponseBody`]: The body type for inbound HTTP responses. -/// - [`http::Response`]: The standard HTTP response type from the `http` crate. -pub type IncomingResponse = http::Response; - type HttpResult = Result; -/// Sends an HTTP request and returns the corresponding [`wasip3::http::types::Response`]. -/// -/// This function converts the provided value into a [`wasip3::http::types::Request`] using the -/// [`IntoRequest`] trait, dispatches it to the WASI HTTP handler, and awaits -/// the resulting response. It provides a convenient high-level interface for -/// issuing HTTP requests within a WASI environment. -pub async fn send(request: impl IntoRequest) -> HttpResult { - let request = request.into_request()?; - let response = wasip3::http::handler::handle(request).await?; - IncomingResponse::from_response(response) -} - -/// A trait for any type that can be converted into a [`wasip3::http::types::Request`]. -/// -/// This trait provides a unified interface for adapting user-defined request -/// types into the lower-level [`wasip3::http::types::Request`] format used by -/// the WASI HTTP subsystem. -/// -/// Implementing `IntoRequest` allows custom builders or wrapper types to -/// interoperate seamlessly with APIs that expect standardized WASI HTTP -/// request objects. -/// -/// # See also -/// - [`FromRequest`]: The inverse conversion trait. -pub trait IntoRequest { - /// Converts `self` into a [`wasip3::http::types::Request`]. - fn into_request(self) -> HttpResult; -} - -/// A trait for any type that can be converted into a [`wasip3::http::types::Response`]. -/// -/// This trait provides a unified interface for adapting user-defined response -/// types into the lower-level [`wasip3::http::types::Response`] format used by -/// the WASI HTTP subsystem. -/// -/// Implementing `IntoResponse` enables ergonomic conversion from domain-level -/// response types or builders into standardized WASI HTTP responses. -/// -/// # See also -/// - [`FromResponse`]: The inverse conversion trait. -pub trait IntoResponse { - /// Converts `self` into a [`wasip3::http::types::Response`]. - fn into_response(self) -> HttpResult; -} - -/// A trait for constructing a value from a [`wasip3::http::types::Request`]. -/// -/// This is the inverse of [`IntoRequest`], allowing higher-level request -/// types to be built from standardized WASI HTTP requests—for example, -/// to parse structured payloads, extract query parameters, or perform -/// request validation. -/// -/// # See also -/// - [`IntoRequest`]: Converts a type into a [`wasip3::http::types::Request`]. -pub trait FromRequest { - /// Attempts to construct `Self` from a [`wasip3::http::types::Request`]. - fn from_request(req: wasip3::http::types::Request) -> HttpResult - where - Self: Sized; -} - -/// A trait for constructing a value from a [`wasip3::http::types::Response`]. -/// -/// This is the inverse of [`IntoResponse`], allowing higher-level response -/// types to be derived from standardized WASI HTTP responses—for example, -/// to deserialize JSON payloads or map responses to domain-specific types. -/// -/// # See also -/// - [`IntoResponse`]: Converts a type into a [`wasip3::http::types::Response`]. -pub trait FromResponse { - /// Attempts to construct `Self` from a [`wasip3::http::types::Response`]. - fn from_response(response: wasip3::http::types::Response) -> HttpResult - where - Self: Sized; -} - /// The error type used for HTTP operations within the WASI environment. /// /// This enum provides a unified representation of all errors that can occur @@ -231,6 +133,157 @@ impl> IntoResponse for Result { } } +/// Sends an HTTP request and returns the corresponding [`wasip3::http::types::Response`]. +/// +/// This function converts the provided value into a [`wasip3::http::types::Request`] using the +/// [`IntoRequest`] trait, dispatches it to the WASI HTTP handler, and awaits +/// the resulting response. It provides a convenient high-level interface for +/// issuing HTTP requests within a WASI environment. +pub async fn send(request: impl IntoRequest) -> HttpResult { + let request = request.into_request()?; + let response = wasip3::http::handler::handle(request).await?; + Response::from_response(response) +} + +/// A type alias for an HTTP request with a customizable body type. +/// +/// This is a convenience wrapper around [`http::Request`], parameterized +/// by the body type `T`. By default, it uses [`IncomingRequestBody`], +/// which represents the standard incoming body used by this runtime. +/// +/// # Type Parameters +/// +/// * `T` — The request body type. Defaults to [`IncomingRequestBody`]. +/// +/// # See also +/// - [`wasip3_http_ext::IncomingRequestBody`]: The body type for inbound HTTP requests. +/// - [`http::Request`]: The standard HTTP request type from the `http` crate. +pub type Request = http::Request; + +/// A type alias for an HTTP response with a customizable body type. +/// +/// This is a convenience wrapper around [`http::Response`], parameterized +/// by the body type `T`. By default, it uses [`IncomingResponseBody`], +/// which represents the standard incoming body type used by this runtime. +/// +/// # Type Parameters +/// +/// * `T` — The response body type. Defaults to [`IncomingResponseBody`]. +/// +/// # See also +/// - [`wasip3_http_ext::IncomingResponseBody`]: The body type for inbound HTTP responses. +/// - [`http::Response`]: The standard HTTP response type from the `http` crate. +pub type Response = http::Response; + +/// A body type representing an empty payload. +/// +/// This is a convenience alias for [`http_body_util::Empty`], +/// used when constructing HTTP requests or responses with no body. +/// +/// # Examples +/// +/// ```ignore +/// use spin_wasip3_http::EmptyBody; +/// +/// let empty = EmptyBody::new(); +/// let response = http::Response::builder() +/// .status(204) +/// .body(empty) +/// .unwrap(); +/// ``` +pub type EmptyBody = http_body_util::Empty; + +/// A body type representing a complete, in-memory payload. +/// +/// This is a convenience alias for [`http_body_util::Full`], used when the +/// entire body is already available as a single value of type `T`. +/// +/// It is typically used for sending small or pre-buffered request or response +/// bodies without the need for streaming. +/// +/// # Type Parameters +/// +/// * `T` — The data type of the full body, such as [`bytes::Bytes`] or [`String`]. +/// +/// # Examples +/// +/// ```ignore +/// use spin_wasip3_http::FullBody; +/// use bytes::Bytes; +/// +/// let body = FullBody::new(Bytes::from("hello")); +/// let request = http::Request::builder() +/// .method("POST") +/// .uri("https://example.com") +/// .body(body) +/// .unwrap(); +/// ``` +pub type FullBody = http_body_util::Full; + +/// A trait for any type that can be converted into a [`wasip3::http::types::Request`]. +/// +/// This trait provides a unified interface for adapting user-defined request +/// types into the lower-level [`wasip3::http::types::Request`] format used by +/// the WASI HTTP subsystem. +/// +/// Implementing `IntoRequest` allows custom builders or wrapper types to +/// interoperate seamlessly with APIs that expect standardized WASI HTTP +/// request objects. +/// +/// # See also +/// - [`FromRequest`]: The inverse conversion trait. +pub trait IntoRequest { + /// Converts `self` into a [`wasip3::http::types::Request`]. + fn into_request(self) -> HttpResult; +} + +/// A trait for any type that can be converted into a [`wasip3::http::types::Response`]. +/// +/// This trait provides a unified interface for adapting user-defined response +/// types into the lower-level [`wasip3::http::types::Response`] format used by +/// the WASI HTTP subsystem. +/// +/// Implementing `IntoResponse` enables ergonomic conversion from domain-level +/// response types or builders into standardized WASI HTTP responses. +/// +/// # See also +/// - [`FromResponse`]: The inverse conversion trait. +pub trait IntoResponse { + /// Converts `self` into a [`wasip3::http::types::Response`]. + fn into_response(self) -> HttpResult; +} + +/// A trait for constructing a value from a [`wasip3::http::types::Request`]. +/// +/// This is the inverse of [`IntoRequest`], allowing higher-level request +/// types to be built from standardized WASI HTTP requests—for example, +/// to parse structured payloads, extract query parameters, or perform +/// request validation. +/// +/// # See also +/// - [`IntoRequest`]: Converts a type into a [`wasip3::http::types::Request`]. +pub trait FromRequest { + /// Attempts to construct `Self` from a [`wasip3::http::types::Request`]. + fn from_request(req: wasip3::http::types::Request) -> HttpResult + where + Self: Sized; +} + +/// A trait for constructing a value from a [`wasip3::http::types::Response`]. +/// +/// This is the inverse of [`IntoResponse`], allowing higher-level response +/// types to be derived from standardized WASI HTTP responses—for example, +/// to deserialize JSON payloads or map responses to domain-specific types. +/// +/// # See also +/// - [`IntoResponse`]: Converts a type into a [`wasip3::http::types::Response`]. +pub trait FromResponse { + /// Attempts to construct `Self` from a [`wasip3::http::types::Response`]. + fn from_response(response: wasip3::http::types::Response) -> HttpResult + where + Self: Sized; +} + impl IntoRequest for http::Request where T: http_body::Body + Any, @@ -389,6 +442,8 @@ where let (response, _future_result) = types::Response::new(headers, Some(body_rx), body_result_rx); + _ = response.set_status_code(self.status().as_u16()); + wit_bindgen::spawn(async move { let mut body = std::pin::pin!(self.into_body()); _ = body_writer.forward_http_body(&mut body).await; @@ -415,3 +470,57 @@ impl FromResponse for IncomingResponseBody { Self::new(response) } } + +/// Helpers for consuming an [`IncomingBody`]. +/// +/// This module provides extension traits and utilities for working with +/// [`IncomingBody`] instances, such as streaming or collecting the entire +/// body into memory. +/// +/// These helpers make it easier to transform low-level streaming body types +/// into higher-level forms (e.g., [`Bytes`]) for simplified data handling. +pub mod body { + use bytes::Bytes; + use http_body_util::{BodyDataStream, BodyExt}; + use wasip3_http_ext::wasip3::http::types::ErrorCode; + use wasip3_http_ext::{IncomingBody, IncomingMessage}; + + /// Extension trait providing convenient methods for consuming an [`IncomingBody`]. + /// + /// This trait defines common patterns for handling HTTP body data in + /// asynchronous contexts. It allows converting the body into a stream + /// or fully collecting it into memory as a [`Bytes`] buffer. + #[allow(async_fn_in_trait)] + pub trait IncomingBodyExt { + /// Convert this [`IncomingBody`] into a [`BodyDataStream`]. + /// + /// This method enables iteration over the body’s data chunks as they + /// arrive, without collecting them all into memory at once. It is + /// suitable for processing large or streaming payloads efficiently. + fn stream(self) -> BodyDataStream + where + Self: Sized; + + /// Consume this [`IncomingBody`] and collect it into a single [`Bytes`] buffer. + /// + /// This method reads the entire body asynchronously and returns the + /// concatenated contents. It is best suited for small or bounded-size + /// payloads where holding all data in memory is acceptable. + async fn bytes(self) -> Result; + } + + impl IncomingBodyExt for IncomingBody { + /// Convert this [`IncomingBody`] into a [`BodyDataStream`]. + fn stream(self) -> BodyDataStream + where + Self: Sized, + { + BodyDataStream::new(self) + } + + /// Collect the [`IncomingBody`] into a single [`Bytes`] buffer. + async fn bytes(self) -> Result { + self.collect().await.map(|c| c.to_bytes()) + } + } +} diff --git a/crates/wasip3-http-ext/Cargo.toml b/crates/wasip3-http-ext/Cargo.toml index 4ffecfe..c3ba356 100644 --- a/crates/wasip3-http-ext/Cargo.toml +++ b/crates/wasip3-http-ext/Cargo.toml @@ -11,7 +11,6 @@ homepage.workspace = true [dependencies] bytes = { workspace = true } http-body = { workspace = true } -http-body-util = { workspace = true } hyperium = { workspace = true } -wasip3 = { workspace = true } +wasip3 = "0.2.0" thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/wasip3-http-ext/src/body_writer.rs b/crates/wasip3-http-ext/src/body_writer.rs index 175dec0..cc5b8ce 100644 --- a/crates/wasip3-http-ext/src/body_writer.rs +++ b/crates/wasip3-http-ext/src/body_writer.rs @@ -1,8 +1,7 @@ -use std::fmt::Debug; - -use http_body::Frame; -use http_body_util::BodyExt as _; +use http_body::{Body as _, Frame}; use hyperium::HeaderMap; +use std::future::poll_fn; +use std::{fmt::Debug, pin}; use wasip3::{ http::types::{ErrorCode, HeaderError, Trailers}, wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter}, @@ -75,15 +74,18 @@ impl BodyWriter { /// trailers) is returned. /// /// If there is an error it is written to to the result future. - pub async fn forward_http_body(mut self, body: &mut T) -> Result + pub async fn forward_http_body(mut self, mut body: &mut T) -> Result where T: http_body::Body + Unpin, T::Data: Into>, T::Error: Into, { let mut total_written = 0; + loop { - match body.frame().await { + let frame = poll_fn(|cx| pin::Pin::new(&mut body).poll_frame(cx)).await; + + match frame { Some(Ok(frame)) => { let written = self.process_http_body_frame(frame).await?; total_written += written as u64; diff --git a/crates/wasip3-http-ext/src/lib.rs b/crates/wasip3-http-ext/src/lib.rs index 2928214..ada1ffe 100644 --- a/crates/wasip3-http-ext/src/lib.rs +++ b/crates/wasip3-http-ext/src/lib.rs @@ -5,7 +5,6 @@ pub mod body_writer; use bytes::Bytes; use helpers::{fields_to_header_map, get_content_length, to_internal_error_code}; use http_body::SizeHint; -use http_body_util::{BodyExt, BodyStream}; use hyperium as http; use std::{ pin::Pin, @@ -17,6 +16,8 @@ use wasip3::{ wit_future, }; +pub use wasip3; + const READ_FRAME_SIZE: usize = 16 * 1024; pub type IncomingRequestBody = IncomingBody; @@ -99,16 +100,6 @@ impl IncomingBody { }) } - pub async fn stream(self) -> BodyStream { - BodyStream::new(self) - } - - pub async fn bytes(self) -> Result { - self.collect().await.map(|c| c.to_bytes()) - } - - // TODO: pub fn take_future() -> result - pub fn take_unstarted(&mut self) -> Option { match self.state { StartedState::Unstarted(_) => { diff --git a/examples/wasip3-http-axum-router/Cargo.toml b/examples/wasip3-http-axum-router/Cargo.toml index 6694d9e..e015e36 100644 --- a/examples/wasip3-http-axum-router/Cargo.toml +++ b/examples/wasip3-http-axum-router/Cargo.toml @@ -9,5 +9,5 @@ crate-type = ["cdylib"] [dependencies] axum = { version = "0.8.1", default-features = false, features = ["json", "macros"] } serde = { version = "1.0.163", features = ["derive"] } -spin-sdk = { path = "../.." } +spin-sdk = { path = "../..", features = ["wasip3-unstable"] } tower-service = "0.3.3" \ No newline at end of file diff --git a/examples/wasip3-http-axum-router/spin.toml b/examples/wasip3-http-axum-router/spin.toml index 94f9af9..c142586 100644 --- a/examples/wasip3-http-axum-router/spin.toml +++ b/examples/wasip3-http-axum-router/spin.toml @@ -13,8 +13,8 @@ route = "/..." component = "axum-router" [component.axum-router] -source = "../../target/wasm32-wasip1/release/axum_router.wasm" +source = "../../target/wasm32-wasip2/release/axum_router.wasm" allowed_outbound_hosts = [] [component.axum-router.build] -command = "cargo build --target wasm32-wasip1 --release" +command = "cargo build --target wasm32-wasip2 --release" watch = ["src/**/*.rs", "Cargo.toml"] diff --git a/examples/wasip3-http-axum-router/src/lib.rs b/examples/wasip3-http-axum-router/src/lib.rs index 5a9db9b..463da17 100644 --- a/examples/wasip3-http-axum-router/src/lib.rs +++ b/examples/wasip3-http-axum-router/src/lib.rs @@ -4,12 +4,12 @@ use axum::{ Json, Router, }; use serde::{Deserialize, Serialize}; -use spin_sdk::http_wasip3::{http_component, IncomingRequest, IntoResponse}; +use spin_sdk::http_wasip3::{http_component, IntoResponse, Request}; use tower_service::Service; /// Sends a request to a URL. #[http_component] -async fn handler(req: IncomingRequest) -> impl IntoResponse { +async fn handler(req: Request) -> impl IntoResponse { Router::new() .route("/", get(root)) .route("/users", post(create_user)) diff --git a/examples/wasip3-http-hello-world/Cargo.toml b/examples/wasip3-http-hello-world/Cargo.toml index b67ccbc..b091358 100644 --- a/examples/wasip3-http-hello-world/Cargo.toml +++ b/examples/wasip3-http-hello-world/Cargo.toml @@ -7,4 +7,4 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -spin-sdk = { path = "../.." } \ No newline at end of file +spin-sdk = { path = "../..", features = ["wasip3-unstable"] } \ No newline at end of file diff --git a/examples/wasip3-http-hello-world/spin.toml b/examples/wasip3-http-hello-world/spin.toml index 30ced24..9d44a64 100644 --- a/examples/wasip3-http-hello-world/spin.toml +++ b/examples/wasip3-http-hello-world/spin.toml @@ -13,8 +13,8 @@ route = "/hello" component = "hello" [component.hello] -source = "../../target/wasm32-wasip1/release/wasip3_hello_world.wasm" +source = "../../target/wasm32-wasip2/release/wasip3_hello_world.wasm" description = "A component that returns hello." [component.hello.build] -command = "cargo build --target wasm32-wasip1 --release" +command = "cargo build --target wasm32-wasip2 --release" watch = ["src/**/*.rs", "Cargo.toml"] \ No newline at end of file diff --git a/examples/wasip3-http-hello-world/src/lib.rs b/examples/wasip3-http-hello-world/src/lib.rs index 760619b..7aa8193 100644 --- a/examples/wasip3-http-hello-world/src/lib.rs +++ b/examples/wasip3-http-hello-world/src/lib.rs @@ -1,7 +1,7 @@ -use spin_sdk::http_wasip3::{http_component, IncomingRequest}; +use spin_sdk::http_wasip3::{http_component, Request}; /// A simple Spin HTTP component. #[http_component] -async fn hello_world(_req: IncomingRequest) -> &'static str { +async fn hello_world(_req: Request) -> &'static str { "Hello, world!" } diff --git a/examples/wasip3-http-send-request/Cargo.toml b/examples/wasip3-http-send-request/Cargo.toml index 83aa3f2..142f6c3 100644 --- a/examples/wasip3-http-send-request/Cargo.toml +++ b/examples/wasip3-http-send-request/Cargo.toml @@ -10,4 +10,4 @@ crate-type = ["cdylib"] anyhow = "1" axum = { version = "0.8.1", default-features = false, features = ["macros"] } http = "1.3.1" -spin-sdk = { path = "../.." } \ No newline at end of file +spin-sdk = { path = "../..", features = ["wasip3-unstable"] } \ No newline at end of file diff --git a/examples/wasip3-http-send-request/spin.toml b/examples/wasip3-http-send-request/spin.toml index bbf944e..6175700 100644 --- a/examples/wasip3-http-send-request/spin.toml +++ b/examples/wasip3-http-send-request/spin.toml @@ -13,11 +13,11 @@ route = "/..." component = "send" [component.send] -source = "../../target/wasm32-wasip1/release/send_request.wasm" +source = "../../target/wasm32-wasip2/release/send_request.wasm" description = "A component that sends a request." allowed_outbound_hosts = [ "https://bytecodealliance.org", ] [component.send.build] -command = "cargo build --target wasm32-wasip1 --release" +command = "cargo build --target wasm32-wasip2 --release" watch = ["src/**/*.rs", "Cargo.toml"] \ No newline at end of file diff --git a/examples/wasip3-http-send-request/src/lib.rs b/examples/wasip3-http-send-request/src/lib.rs index 3ad58dd..331caaf 100644 --- a/examples/wasip3-http-send-request/src/lib.rs +++ b/examples/wasip3-http-send-request/src/lib.rs @@ -1,10 +1,9 @@ -use axum::body::Body; -use spin_sdk::http_wasip3::{http_component, send, IncomingRequest, IntoResponse, Result}; +use spin_sdk::http_wasip3::{http_component, send, EmptyBody, IntoResponse, Request, Result}; /// Sends a request to a URL. #[http_component] -async fn send_request(_req: IncomingRequest) -> Result { - let outgoing = http::Request::get("https://bytecodealliance.org").body(Body::empty())?; +async fn send_request(_req: Request) -> Result { + let outgoing = http::Request::get("https://bytecodealliance.org").body(EmptyBody::new())?; Ok(send(outgoing).await?) } diff --git a/src/lib.rs b/src/lib.rs index da1846a..1443c4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,12 +19,13 @@ pub mod llm; pub use spin_macro::*; -/// WasiP3 HTTP APIs and helpers +/// WASIp3 HTTP APIs and helpers +#[cfg(feature = "wasip3-unstable")] pub mod http_wasip3 { - /// Re-exports the helpers types for converting between WasiP3 HTTP types and + /// Re-exports the helpers types for converting between WASIp3 HTTP types and /// Rust ecosystem HTTP types. pub use spin_wasip3_http::*; - /// Re-exports the macro to enable WasiP3 HTTP handlers + /// Re-exports the macro to enable WASIp3 HTTP handlers pub use spin_wasip3_http_macro::*; } From af614bf9e00ea6707e69efb7085ed36108d667f0 Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Wed, 22 Oct 2025 13:44:56 -0600 Subject: [PATCH 3/5] Delete wasip3-http-ext and use upstream wasip3 additions Signed-off-by: Brian Hardock --- Cargo.lock | 21 +- crates/spin-wasip3-http-macro/src/lib.rs | 11 + crates/spin-wasip3-http/Cargo.toml | 2 +- crates/spin-wasip3-http/src/lib.rs | 343 ++++++--------------- crates/wasip3-http-ext/Cargo.toml | 16 - crates/wasip3-http-ext/src/body_writer.rs | 146 --------- crates/wasip3-http-ext/src/lib.rs | 356 ---------------------- 7 files changed, 120 insertions(+), 775 deletions(-) delete mode 100644 crates/wasip3-http-ext/Cargo.toml delete mode 100644 crates/wasip3-http-ext/src/body_writer.rs delete mode 100644 crates/wasip3-http-ext/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 713b986..9051680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2306,7 +2306,7 @@ dependencies = [ "http 1.3.1", "http-body 1.0.1", "http-body-util", - "wasip3-http-ext", + "wasip3", ] [[package]] @@ -2819,10 +2819,14 @@ dependencies = [ [[package]] name = "wasip3" -version = "0.2.0+wasi-0.3.0-rc-2025-09-16" +version = "0.2.1+wasi-0.3.0-rc-2025-09-16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c5ffefc208bc11080d0e6a44e1807cbbd3fc67dafd20078fffb4598421e33" +checksum = "cbb2796323e2357ae2d4ba2b781a0392b533f40a5b9f534eef49b23e54186d64" dependencies = [ + "bytes", + "http 1.3.1", + "http-body 1.0.1", + "thiserror 2.0.17", "wit-bindgen 0.46.0", ] @@ -2833,17 +2837,6 @@ dependencies = [ "spin-sdk", ] -[[package]] -name = "wasip3-http-ext" -version = "5.0.0" -dependencies = [ - "bytes", - "http 1.3.1", - "http-body 1.0.1", - "thiserror 2.0.17", - "wasip3", -] - [[package]] name = "wasm-bindgen" version = "0.2.100" diff --git a/crates/spin-wasip3-http-macro/src/lib.rs b/crates/spin-wasip3-http-macro/src/lib.rs index 56e32eb..e4ad70c 100644 --- a/crates/spin-wasip3-http-macro/src/lib.rs +++ b/crates/spin-wasip3-http-macro/src/lib.rs @@ -22,6 +22,17 @@ use quote::quote; /// /// If the function is not asynchronous, the macro emits a compile-time error. /// +/// # Example +/// +/// ```ignore +/// use spin_sdk::http_wasip3::{http_component, Request, IntoResponse}; +/// +/// #[http_component] +/// async fn my_handler(request: Request) -> impl IntoResponse { +/// // Your logic goes here +/// } +/// ``` +/// /// # Generated Code /// /// The macro expands into a module containing a `Spin` struct that implements the diff --git a/crates/spin-wasip3-http/Cargo.toml b/crates/spin-wasip3-http/Cargo.toml index 6b3a625..2b85aba 100644 --- a/crates/spin-wasip3-http/Cargo.toml +++ b/crates/spin-wasip3-http/Cargo.toml @@ -14,4 +14,4 @@ bytes = { workspace = true } http-body = { workspace = true } http-body-util = { workspace = true } hyperium = { workspace = true } -wasip3-http-ext = { version = "5.0.0", path = "../wasip3-http-ext" } \ No newline at end of file +wasip3 = { version = "0.2.1", features = ["http-compat"] } \ No newline at end of file diff --git a/crates/spin-wasip3-http/src/lib.rs b/crates/spin-wasip3-http/src/lib.rs index ac95e4c..4b58e18 100644 --- a/crates/spin-wasip3-http/src/lib.rs +++ b/crates/spin-wasip3-http/src/lib.rs @@ -3,18 +3,18 @@ #![deny(missing_docs)] #[doc(hidden)] -pub use wasip3_http_ext::wasip3; +pub use wasip3; use hyperium as http; use std::any::Any; -use wasip3::{http::types, wit_bindgen}; -use wasip3_http_ext::body_writer::BodyWriter; -use wasip3_http_ext::helpers::{ - header_map_to_wasi, method_from_wasi, method_to_wasi, scheme_from_wasi, scheme_to_wasi, - to_internal_error_code, +pub use wasip3::http_compat::{Request, Response}; +use wasip3::{ + http::types, + http_compat::{ + http_from_wasi_request, http_from_wasi_response, http_into_wasi_request, + http_into_wasi_response, + }, }; -use wasip3_http_ext::RequestOptionsExtension; -use wasip3_http_ext::{IncomingRequestBody, IncomingResponseBody}; /// A alias for [`std::result::Result`] that uses [`Error`] as the default error type. /// @@ -31,20 +31,8 @@ type HttpResult = Result; /// WASI-level error codes, dynamic runtime failures, or full HTTP responses /// returned as error results. /// -/// # Variants -/// -/// - [`Error::ErrorCode`]: Wraps a low-level [`wasip3::http::types::ErrorCode`] -/// reported by the WASI HTTP runtime (e.g. connection errors, protocol errors). -/// -/// - [`Error::Other`]: Represents an arbitrary dynamic error implementing -/// [`std::error::Error`]. This allows integration with external libraries or -/// application-specific failure types. -/// -/// - [`Error::Response`]: Contains a full [`wasip3::http::types::Response`] -/// representing an HTTP-level error (for example, a `4xx` or `5xx` response -/// that should be treated as an error condition). -/// /// # See also +/// - [`http::Error`]: Error type originating from the [`http`] crate. /// - [`wasip3::http::types::ErrorCode`]: Standard WASI HTTP error codes. /// - [`wasip3::http::types::Response`]: Used when an error represents an HTTP response body. #[derive(Debug)] @@ -54,11 +42,17 @@ pub enum Error { /// Wraps [`wasip3::http::types::ErrorCode`] to represent /// transport-level or protocol-level failures. ErrorCode(wasip3::http::types::ErrorCode), + /// An error originating from the [`http`] crate. + /// + /// Covers errors encountered during the construction, + /// parsing, or validation of [`http`] types (e.g. invalid headers, + /// malformed URIs, or protocol violations). + HttpError(http::Error), /// A dynamic application or library error. /// /// Used for any runtime error that implements [`std::error::Error`], /// allowing flexibility for different error sources. - Other(Box), + Other(Box), /// An HTTP response treated as an error. /// /// Contains a full [`wasip3::http::types::Response`], such as @@ -71,10 +65,11 @@ impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::ErrorCode(e) => write!(f, "{e}"), + Error::HttpError(e) => write!(f, "{e}"), Error::Other(e) => write!(f, "{e}"), - Error::Response(e) => match http::StatusCode::from_u16(e.get_status_code()) { + Error::Response(resp) => match http::StatusCode::from_u16(resp.get_status_code()) { Ok(status) => write!(f, "{status}"), - Err(e) => write!(f, "{e}"), + Err(_) => write!(f, "invalid status code {}", resp.get_status_code()), }, } } @@ -83,8 +78,8 @@ impl std::fmt::Display for Error { impl std::error::Error for Error {} impl From for Error { - fn from(_err: http::Error) -> Error { - todo!("map to specific error codes") + fn from(err: http::Error) -> Error { + Error::HttpError(err) } } @@ -125,6 +120,15 @@ impl> IntoResponse for Result { Err(err) => match err.into() { Error::ErrorCode(code) => Err(code), Error::Response(resp) => Ok(resp), + Error::HttpError(err) => match err { + err if err.is::() => { + Err(types::ErrorCode::HttpRequestMethodInvalid) + } + err if err.is::() => { + Err(types::ErrorCode::HttpRequestUriInvalid) + } + err => Err(types::ErrorCode::InternalError(Some(err.to_string()))), + }, Error::Other(other) => { Err(types::ErrorCode::InternalError(Some(other.to_string()))) } @@ -145,36 +149,6 @@ pub async fn send(request: impl IntoRequest) -> HttpResult { Response::from_response(response) } -/// A type alias for an HTTP request with a customizable body type. -/// -/// This is a convenience wrapper around [`http::Request`], parameterized -/// by the body type `T`. By default, it uses [`IncomingRequestBody`], -/// which represents the standard incoming body used by this runtime. -/// -/// # Type Parameters -/// -/// * `T` — The request body type. Defaults to [`IncomingRequestBody`]. -/// -/// # See also -/// - [`wasip3_http_ext::IncomingRequestBody`]: The body type for inbound HTTP requests. -/// - [`http::Request`]: The standard HTTP request type from the `http` crate. -pub type Request = http::Request; - -/// A type alias for an HTTP response with a customizable body type. -/// -/// This is a convenience wrapper around [`http::Response`], parameterized -/// by the body type `T`. By default, it uses [`IncomingResponseBody`], -/// which represents the standard incoming body type used by this runtime. -/// -/// # Type Parameters -/// -/// * `T` — The response body type. Defaults to [`IncomingResponseBody`]. -/// -/// # See also -/// - [`wasip3_http_ext::IncomingResponseBody`]: The body type for inbound HTTP responses. -/// - [`http::Response`]: The standard HTTP response type from the `http` crate. -pub type Response = http::Response; - /// A body type representing an empty payload. /// /// This is a convenience alias for [`http_body_util::Empty`], @@ -201,10 +175,6 @@ pub type EmptyBody = http_body_util::Empty; /// It is typically used for sending small or pre-buffered request or response /// bodies without the need for streaming. /// -/// # Type Parameters -/// -/// * `T` — The data type of the full body, such as [`bytes::Bytes`] or [`String`]. -/// /// # Examples /// /// ```ignore @@ -220,6 +190,34 @@ pub type EmptyBody = http_body_util::Empty; /// ``` pub type FullBody = http_body_util::Full; +/// A trait for constructing a value from a [`wasip3::http::types::Request`]. +/// +/// This is the inverse of [`IntoRequest`], allowing higher-level request +/// types to be built from standardized WASI HTTP requests—for example, +/// to parse structured payloads, extract query parameters, or perform +/// request validation. +/// +/// # See also +/// - [`IntoRequest`]: Converts a type into a [`wasip3::http::types::Request`]. +pub trait FromRequest { + /// Attempts to construct `Self` from a [`wasip3::http::types::Request`]. + fn from_request(req: wasip3::http::types::Request) -> HttpResult + where + Self: Sized; +} + +impl FromRequest for types::Request { + fn from_request(req: types::Request) -> HttpResult { + Ok(req) + } +} + +impl FromRequest for Request { + fn from_request(req: types::Request) -> HttpResult { + http_from_wasi_request(req) + } +} + /// A trait for any type that can be converted into a [`wasip3::http::types::Request`]. /// /// This trait provides a unified interface for adapting user-defined request @@ -237,36 +235,15 @@ pub trait IntoRequest { fn into_request(self) -> HttpResult; } -/// A trait for any type that can be converted into a [`wasip3::http::types::Response`]. -/// -/// This trait provides a unified interface for adapting user-defined response -/// types into the lower-level [`wasip3::http::types::Response`] format used by -/// the WASI HTTP subsystem. -/// -/// Implementing `IntoResponse` enables ergonomic conversion from domain-level -/// response types or builders into standardized WASI HTTP responses. -/// -/// # See also -/// - [`FromResponse`]: The inverse conversion trait. -pub trait IntoResponse { - /// Converts `self` into a [`wasip3::http::types::Response`]. - fn into_response(self) -> HttpResult; -} - -/// A trait for constructing a value from a [`wasip3::http::types::Request`]. -/// -/// This is the inverse of [`IntoRequest`], allowing higher-level request -/// types to be built from standardized WASI HTTP requests—for example, -/// to parse structured payloads, extract query parameters, or perform -/// request validation. -/// -/// # See also -/// - [`IntoRequest`]: Converts a type into a [`wasip3::http::types::Request`]. -pub trait FromRequest { - /// Attempts to construct `Self` from a [`wasip3::http::types::Request`]. - fn from_request(req: wasip3::http::types::Request) -> HttpResult - where - Self: Sized; +impl IntoRequest for http::Request +where + T: http_body::Body + Any, + T::Data: Into>, + T::Error: Into>, +{ + fn into_request(self) -> HttpResult { + http_into_wasi_request(self) + } } /// A trait for constructing a value from a [`wasip3::http::types::Response`]. @@ -284,116 +261,26 @@ pub trait FromResponse { Self: Sized; } -impl IntoRequest for http::Request -where - T: http_body::Body + Any, - T::Data: Into>, - T::Error: Into>, -{ - fn into_request(mut self) -> HttpResult { - if let Some(incoming_body) = - (&mut self as &mut dyn Any).downcast_mut::() - { - if let Some(request) = incoming_body.take_unstarted() { - return Ok(request); - } - } - - let (parts, body) = self.into_parts(); - - let options = parts - .extensions - .get::() - .cloned() - .map(|o| o.0); - - let headers = header_map_to_wasi(&parts.headers)?; - - let (body_writer, contents_rx, trailers_rx) = BodyWriter::new(); - - let (req, _result) = types::Request::new(headers, Some(contents_rx), trailers_rx, options); - - req.set_method(&method_to_wasi(&parts.method)) - .map_err(|()| types::ErrorCode::HttpRequestMethodInvalid)?; - - let scheme = parts.uri.scheme().map(scheme_to_wasi); - req.set_scheme(scheme.as_ref()) - .map_err(|()| types::ErrorCode::HttpProtocolError)?; - - req.set_authority(parts.uri.authority().map(|a| a.as_str())) - .map_err(|()| types::ErrorCode::HttpRequestUriInvalid)?; - - req.set_path_with_query(parts.uri.path_and_query().map(|pq| pq.as_str())) - .map_err(|()| types::ErrorCode::HttpRequestUriInvalid)?; - - wit_bindgen::spawn(async move { - let mut body = std::pin::pin!(body); - _ = body_writer.forward_http_body(&mut body).await; - }); - - Ok(req) - } -} - -impl FromRequest for types::Request { - fn from_request(req: types::Request) -> HttpResult { - Ok(req) - } -} - -impl FromRequest for http::Request { - fn from_request(req: types::Request) -> HttpResult { - let uri = { - let mut builder = http::Uri::builder(); - if let Some(scheme) = req.get_scheme() { - builder = builder.scheme(scheme_from_wasi(scheme)?); - } - if let Some(authority) = req.get_authority() { - builder = builder.authority(authority); - } - if let Some(path_and_query) = req.get_path_with_query() { - builder = builder.path_and_query(path_and_query); - } - builder - .build() - .map_err(|_| types::ErrorCode::HttpRequestUriInvalid)? - }; - - let mut builder = http::Request::builder() - .method(method_from_wasi(req.get_method())?) - .uri(uri); - - if let Some(options) = req.get_options().map(RequestOptionsExtension) { - builder = builder.extension(options); - } - - for (k, v) in req.get_headers().copy_all() { - builder = builder.header(k, v); - } - - let body = T::from_request(req)?; - - builder.body(body).map_err(to_internal_error_code) // TODO: downcast to more specific http error codes - } -} - -impl FromResponse for http::Response { +impl FromResponse for Response { fn from_response(resp: types::Response) -> HttpResult { - let mut builder = http::Response::builder().status(resp.get_status_code()); - - for (k, v) in resp.get_headers().copy_all() { - builder = builder.header(k, v); - } - - let body = T::from_response(resp)?; - builder.body(body).map_err(to_internal_error_code) // TODO: downcast to more specific http error codes + http_from_wasi_response(resp) } } -impl FromRequest for () { - fn from_request(_req: types::Request) -> HttpResult { - Ok(()) - } +/// A trait for any type that can be converted into a [`wasip3::http::types::Response`]. +/// +/// This trait provides a unified interface for adapting user-defined response +/// types into the lower-level [`wasip3::http::types::Response`] format used by +/// the WASI HTTP subsystem. +/// +/// Implementing `IntoResponse` enables ergonomic conversion from domain-level +/// response types or builders into standardized WASI HTTP responses. +/// +/// # See also +/// - [`FromResponse`]: The inverse conversion trait. +pub trait IntoResponse { + /// Converts `self` into a [`wasip3::http::types::Response`]. + fn into_response(self) -> HttpResult; } impl IntoResponse for types::Response { @@ -402,9 +289,19 @@ impl IntoResponse for types::Response { } } -impl IntoResponse for (http::StatusCode, T) { +impl IntoResponse for (http::StatusCode, T) +where + T: http_body::Body + Any, + T::Data: Into>, + T::Error: Into>, +{ fn into_response(self) -> HttpResult { - unreachable!() + http_into_wasi_response( + http::Response::builder() + .status(self.0) + .body(self.1) + .unwrap(), + ) } } @@ -426,48 +323,8 @@ where T::Data: Into>, T::Error: Into>, { - fn into_response(mut self) -> HttpResult { - if let Some(incoming_body) = - (&mut self as &mut dyn Any).downcast_mut::() - { - if let Some(response) = incoming_body.take_unstarted() { - return Ok(response); - } - } - - let headers = header_map_to_wasi(self.headers())?; - - let (body_writer, body_rx, body_result_rx) = BodyWriter::new(); - - let (response, _future_result) = - types::Response::new(headers, Some(body_rx), body_result_rx); - - _ = response.set_status_code(self.status().as_u16()); - - wit_bindgen::spawn(async move { - let mut body = std::pin::pin!(self.into_body()); - _ = body_writer.forward_http_body(&mut body).await; - }); - - Ok(response) - } -} - -impl FromRequest for IncomingRequestBody { - fn from_request(req: types::Request) -> HttpResult - where - Self: Sized, - { - Self::new(req) - } -} - -impl FromResponse for IncomingResponseBody { - fn from_response(response: types::Response) -> HttpResult - where - Self: Sized, - { - Self::new(response) + fn into_response(self) -> HttpResult { + http_into_wasi_response(self) } } @@ -482,8 +339,10 @@ impl FromResponse for IncomingResponseBody { pub mod body { use bytes::Bytes; use http_body_util::{BodyDataStream, BodyExt}; - use wasip3_http_ext::wasip3::http::types::ErrorCode; - use wasip3_http_ext::{IncomingBody, IncomingMessage}; + use wasip3::{ + http::types::ErrorCode, + http_compat::{IncomingBody, IncomingMessage}, + }; /// Extension trait providing convenient methods for consuming an [`IncomingBody`]. /// diff --git a/crates/wasip3-http-ext/Cargo.toml b/crates/wasip3-http-ext/Cargo.toml deleted file mode 100644 index c3ba356..0000000 --- a/crates/wasip3-http-ext/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "wasip3-http-ext" -version.workspace = true -authors.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -homepage.workspace = true - -[dependencies] -bytes = { workspace = true } -http-body = { workspace = true } -hyperium = { workspace = true } -wasip3 = "0.2.0" -thiserror = { workspace = true } \ No newline at end of file diff --git a/crates/wasip3-http-ext/src/body_writer.rs b/crates/wasip3-http-ext/src/body_writer.rs deleted file mode 100644 index cc5b8ce..0000000 --- a/crates/wasip3-http-ext/src/body_writer.rs +++ /dev/null @@ -1,146 +0,0 @@ -use http_body::{Body as _, Frame}; -use hyperium::HeaderMap; -use std::future::poll_fn; -use std::{fmt::Debug, pin}; -use wasip3::{ - http::types::{ErrorCode, HeaderError, Trailers}, - wit_bindgen::{FutureReader, FutureWriter, StreamReader, StreamWriter}, - wit_future, wit_stream, -}; - -use crate::helpers::header_map_to_fields; - -type BoxError = Box; - -pub type BodyResult = Result, ErrorCode>; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - /// The [`http_body::Body`] returned an error. - #[error("body error: {0}")] - HttpBody(#[source] BoxError), - - /// Received trailers were rejected by [`Trailers::from_list`]. - #[error("invalid trailers: {0}")] - InvalidTrailers(#[source] HeaderError), - - /// The result future reader end was closed (dropped). - /// - /// The result that couldn't be written is returned. - #[error("result future reader closed")] - ResultReaderClosed(BodyResult), - - /// The stream reader end was closed (dropped). - /// - /// The number of bytes written successfully is returned as `written` and - /// the bytes that couldn't be written are returned as `unwritten`. - #[error("stream reader closed")] - StreamReaderClosed { written: usize, unwritten: Vec }, -} - -/// BodyWriter coordinates a [`StreamWriter`] and [`FutureWriter`] associated -/// with the write end of a `wasi:http` `Request` or `Response` body. -pub struct BodyWriter { - pub stream_writer: StreamWriter, - pub result_writer: FutureWriter, - pub trailers: HeaderMap, -} - -impl BodyWriter { - /// Returns a new writer and the matching stream and result future readers, - /// which will typically be used to create a `wasi:http` `Request` or - /// `Response`. - pub fn new() -> (Self, StreamReader, FutureReader) { - let (stream_writer, stream_reader) = wit_stream::new(); - let (result_writer, result_reader) = - // TODO: is there a more appropriate ErrorCode? - wit_future::new(|| Err(ErrorCode::InternalError(Some("body writer dropped".into())))); - ( - Self { - stream_writer, - result_writer, - trailers: Default::default(), - }, - stream_reader, - result_reader, - ) - } - - /// Forwards the given [`http_body::Body`] to this writer. - /// - /// This copies all data frames from the body to this writer's stream and - /// then writes any trailers from the body to the result future. On success - /// the number of data bytes written to the stream (which does not including - /// trailers) is returned. - /// - /// If there is an error it is written to to the result future. - pub async fn forward_http_body(mut self, mut body: &mut T) -> Result - where - T: http_body::Body + Unpin, - T::Data: Into>, - T::Error: Into, - { - let mut total_written = 0; - - loop { - let frame = poll_fn(|cx| pin::Pin::new(&mut body).poll_frame(cx)).await; - - match frame { - Some(Ok(frame)) => { - let written = self.process_http_body_frame(frame).await?; - total_written += written as u64; - } - Some(Err(err)) => { - let err = err.into(); - // TODO: consider if there are better ErrorCode mappings - let error_code = ErrorCode::InternalError(Some(err.to_string())); - // TODO: log result_writer.write errors? - _ = self.result_writer.write(Err(error_code)).await; - return Err(Error::HttpBody(err)); - } - None => break, - } - } - let maybe_trailers = if self.trailers.is_empty() { - None - } else { - Some(header_map_to_fields(self.trailers).map_err(Error::InvalidTrailers)?) - }; - match self.result_writer.write(Ok(maybe_trailers)).await { - Ok(()) => Ok(total_written), - Err(err) => Err(Error::ResultReaderClosed(err.value)), - } - } - - /// Processes a [`http_body::Frame`]. - /// - /// - If the frame contains data, the data is written to this writer's - /// stream and the size of the written data is returned. - /// - If the frame contains trailers they are added to [`Self::trailers`] - /// and `Ok(0)` is returned. - pub async fn process_http_body_frame(&mut self, frame: Frame) -> Result - where - T: Into>, - { - // Frame is a pseudo-enum which is either 'data' or 'trailers' - if frame.is_data() { - let data = frame.into_data().unwrap_or_else(|_| unreachable!()).into(); - let data_len = data.len(); - // write_all returns any unwritten data if the read end is dropped - let unwritten = self.stream_writer.write_all(data).await; - if !unwritten.is_empty() { - return Err(Error::StreamReaderClosed { - written: data_len - unwritten.len(), - unwritten, - }); - } - Ok(data_len) - } else if frame.is_trailers() { - let trailers = frame.into_trailers().unwrap_or_else(|_| unreachable!()); - self.trailers.extend(trailers); - Ok(0) - } else { - unreachable!("Frames are data or trailers"); - } - } -} diff --git a/crates/wasip3-http-ext/src/lib.rs b/crates/wasip3-http-ext/src/lib.rs deleted file mode 100644 index ada1ffe..0000000 --- a/crates/wasip3-http-ext/src/lib.rs +++ /dev/null @@ -1,356 +0,0 @@ -//! Extension types for wasip3::http - -pub mod body_writer; - -use bytes::Bytes; -use helpers::{fields_to_header_map, get_content_length, to_internal_error_code}; -use http_body::SizeHint; -use hyperium as http; -use std::{ - pin::Pin, - task::{self, Poll}, -}; -use wasip3::{ - http::types::{self, ErrorCode}, - wit_bindgen::{self, StreamResult}, - wit_future, -}; - -pub use wasip3; - -const READ_FRAME_SIZE: usize = 16 * 1024; - -pub type IncomingRequestBody = IncomingBody; -pub type IncomingResponseBody = IncomingBody; - -pub struct RequestOptionsExtension(pub types::RequestOptions); - -impl Clone for RequestOptionsExtension { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -pub trait IncomingMessage: Unpin { - fn get_headers(&self) -> types::Headers; - - fn consume_body( - self, - res: wit_bindgen::FutureReader>, - ) -> ( - wit_bindgen::StreamReader, - wit_bindgen::FutureReader, ErrorCode>>, - ); -} - -impl IncomingMessage for types::Request { - fn get_headers(&self) -> types::Headers { - self.get_headers() - } - - fn consume_body( - self, - res: wit_bindgen::FutureReader>, - ) -> ( - wit_bindgen::StreamReader, - wit_bindgen::FutureReader, ErrorCode>>, - ) { - Self::consume_body(self, res) - } -} - -impl IncomingMessage for types::Response { - fn get_headers(&self) -> types::Headers { - self.get_headers() - } - - fn consume_body( - self, - res: wit_bindgen::FutureReader>, - ) -> ( - wit_bindgen::StreamReader, - wit_bindgen::FutureReader, ErrorCode>>, - ) { - Self::consume_body(self, res) - } -} - -/// A stream of Bytes, used when receiving bodies from the network. -pub struct IncomingBody { - state: StartedState, - content_length: Option, -} - -enum StartedState { - Unstarted(T), - Started { - #[allow(dead_code)] - result: wit_bindgen::FutureWriter>, - state: IncomingState, - }, - Empty, -} - -impl IncomingBody { - pub fn new(msg: T) -> Result { - let content_length = get_content_length(msg.get_headers())?; - Ok(Self { - state: StartedState::Unstarted(msg), - content_length, - }) - } - - pub fn take_unstarted(&mut self) -> Option { - match self.state { - StartedState::Unstarted(_) => { - let StartedState::Unstarted(msg) = - std::mem::replace(&mut self.state, StartedState::Empty) - else { - unreachable!(); - }; - Some(msg) - } - _ => None, - } - } - - fn ensure_started(&mut self) -> Result<&mut IncomingState, ErrorCode> { - if let StartedState::Unstarted(_) = self.state { - let msg = self.take_unstarted().unwrap(); - let (result, reader) = wit_future::new(|| Ok(())); - let (stream, trailers) = msg.consume_body(reader); - self.state = StartedState::Started { - result, - state: IncomingState::Ready { stream, trailers }, - }; - }; - match &mut self.state { - StartedState::Started { state, .. } => Ok(state), - StartedState::Unstarted(_) => unreachable!(), - StartedState::Empty => Err(to_internal_error_code( - "cannot use IncomingBody after call to take_unstarted", - )), - } - } -} - -enum IncomingState { - Ready { - stream: wit_bindgen::StreamReader, - trailers: wit_bindgen::FutureReader, ErrorCode>>, - }, - Reading(Pin + 'static + Send>>), - Done, -} - -enum ReadResult { - Trailers(Result, ErrorCode>), - BodyChunk { - chunk: Vec, - stream: wit_bindgen::StreamReader, - trailers: wit_bindgen::FutureReader, ErrorCode>>, - }, -} - -impl http_body::Body for IncomingBody { - type Data = Bytes; - type Error = ErrorCode; - - fn poll_frame( - mut self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> Poll, Self::Error>>> { - let state = self.ensure_started()?; - loop { - match state { - IncomingState::Ready { .. } => { - let IncomingState::Ready { - mut stream, - trailers, - } = std::mem::replace(state, IncomingState::Done) - else { - unreachable!(); - }; - *state = IncomingState::Reading(Box::pin(async move { - let (result, chunk) = - stream.read(Vec::with_capacity(READ_FRAME_SIZE)).await; - match result { - StreamResult::Complete(_n) => ReadResult::BodyChunk { - chunk, - stream, - trailers, - }, - StreamResult::Cancelled => unreachable!(), - StreamResult::Dropped => ReadResult::Trailers(trailers.await), - } - })); - } - IncomingState::Reading(future) => { - match std::task::ready!(future.as_mut().poll(cx)) { - ReadResult::BodyChunk { - chunk, - stream, - trailers, - } => { - *state = IncomingState::Ready { stream, trailers }; - break Poll::Ready(Some(Ok(http_body::Frame::data(chunk.into())))); - } - ReadResult::Trailers(trailers) => { - *state = IncomingState::Done; - match trailers { - Ok(Some(fields)) => { - let trailers = fields_to_header_map(fields)?; - break Poll::Ready(Some(Ok(http_body::Frame::trailers( - trailers, - )))); - } - Ok(None) => {} - Err(e) => { - break Poll::Ready(Some(Err(e))); - } - } - } - } - } - IncomingState::Done => break Poll::Ready(None), - } - } - } - - fn is_end_stream(&self) -> bool { - matches!( - self.state, - StartedState::Started { - state: IncomingState::Done, - .. - } - ) - } - - fn size_hint(&self) -> SizeHint { - let Some(n) = self.content_length else { - return SizeHint::default(); - }; - let mut size_hint = SizeHint::new(); - size_hint.set_lower(0); - size_hint.set_upper(n); - size_hint - } -} - -pub mod helpers { - use super::*; - - pub fn get_content_length(headers: types::Headers) -> Result, ErrorCode> { - let values = headers.get(http::header::CONTENT_LENGTH.as_str()); - if values.len() > 1 { - return Err(to_internal_error_code("multiple content-length values")); - } - let Some(value_bytes) = values.into_iter().next() else { - return Ok(None); - }; - let value_str = std::str::from_utf8(&value_bytes).map_err(to_internal_error_code)?; - let value_i64: i64 = value_str.parse().map_err(to_internal_error_code)?; - let value = value_i64.try_into().map_err(to_internal_error_code)?; - Ok(Some(value)) - } - - pub fn fields_to_header_map(headers: types::Headers) -> Result { - headers - .copy_all() - .into_iter() - .try_fold(http::HeaderMap::new(), |mut map, (k, v)| { - let v = http::HeaderValue::from_bytes(&v).map_err(to_internal_error_code)?; - let k: http::HeaderName = k.parse().map_err(to_internal_error_code)?; - map.append(k, v); - Ok(map) - }) - } - - pub fn scheme_from_wasi(scheme: types::Scheme) -> Result { - match scheme { - types::Scheme::Http => Ok(http::uri::Scheme::HTTP), - types::Scheme::Https => Ok(http::uri::Scheme::HTTPS), - types::Scheme::Other(s) => s - .parse() - .map_err(|_| types::ErrorCode::HttpRequestUriInvalid), - } - } - - pub fn scheme_to_wasi(scheme: &http::uri::Scheme) -> types::Scheme { - match scheme { - s if s == &http::uri::Scheme::HTTP => types::Scheme::Http, - s if s == &http::uri::Scheme::HTTPS => types::Scheme::Https, - other => types::Scheme::Other(other.to_string()), - } - } - - pub fn method_from_wasi(method: types::Method) -> Result { - match method { - types::Method::Get => Ok(http::Method::GET), - types::Method::Post => Ok(http::Method::POST), - types::Method::Put => Ok(http::Method::PUT), - types::Method::Delete => Ok(http::Method::DELETE), - types::Method::Patch => Ok(http::Method::PATCH), - types::Method::Head => Ok(http::Method::HEAD), - types::Method::Options => Ok(http::Method::OPTIONS), - types::Method::Connect => Ok(http::Method::CONNECT), - types::Method::Trace => Ok(http::Method::TRACE), - types::Method::Other(o) => http::Method::from_bytes(o.as_bytes()) - .map_err(|_| types::ErrorCode::HttpRequestMethodInvalid), - } - } - - pub fn method_to_wasi(method: &http::Method) -> types::Method { - match method { - &http::Method::GET => types::Method::Get, - &http::Method::POST => types::Method::Post, - &http::Method::PUT => types::Method::Put, - &http::Method::DELETE => types::Method::Delete, - &http::Method::PATCH => types::Method::Patch, - &http::Method::HEAD => types::Method::Head, - &http::Method::OPTIONS => types::Method::Options, - &http::Method::CONNECT => types::Method::Connect, - &http::Method::TRACE => types::Method::Trace, - other => types::Method::Other(other.to_string()), - } - } - - pub fn header_map_to_wasi(map: &http::HeaderMap) -> Result { - types::Fields::from_list( - &map.iter() - .map(|(k, v)| (k.to_string(), v.as_ref().to_vec())) - .collect::>(), - ) - .map_err(to_internal_error_code) - } - - pub fn header_map_to_field_entries( - map: http::HeaderMap, - ) -> impl Iterator)> { - // https://docs.rs/http/1.3.1/http/header/struct.HeaderMap.html#method.into_iter-2 - // For each yielded item that has None provided for the HeaderName, then - // the associated header name is the same as that of the previously - // yielded item. The first yielded item will have HeaderName set. - let mut last_name = None; - map.into_iter().map(move |(name, value)| { - if name.is_some() { - last_name = name; - } - let name = last_name - .as_ref() - .expect("HeaderMap::into_iter always returns Some(name) before None"); - let value = bytes::Bytes::from_owner(value).to_vec(); - (name.as_str().into(), value) - }) - } - - pub fn header_map_to_fields(map: http::HeaderMap) -> Result { - let entries = Vec::from_iter(header_map_to_field_entries(map)); - types::Fields::from_list(&entries) - } - - pub fn to_internal_error_code(e: impl ::std::fmt::Display) -> ErrorCode { - ErrorCode::InternalError(Some(e.to_string())) - } -} From bc2ba7a1d1c4943518df995f03cfa13d9736414c Mon Sep 17 00:00:00 2001 From: itowlson Date: Fri, 24 Oct 2025 12:26:19 +1300 Subject: [PATCH 4/5] Concurrent outbound HTTP P3 example Signed-off-by: itowlson --- .../.gitignore | 2 + .../Cargo.lock | 1368 +++++++++++++++++ .../Cargo.toml | 18 + .../spin.toml | 20 + .../src/lib.rs | 30 + 5 files changed, 1438 insertions(+) create mode 100644 examples/wasip3-concurrent-outbound-http-calls/.gitignore create mode 100644 examples/wasip3-concurrent-outbound-http-calls/Cargo.lock create mode 100644 examples/wasip3-concurrent-outbound-http-calls/Cargo.toml create mode 100644 examples/wasip3-concurrent-outbound-http-calls/spin.toml create mode 100644 examples/wasip3-concurrent-outbound-http-calls/src/lib.rs diff --git a/examples/wasip3-concurrent-outbound-http-calls/.gitignore b/examples/wasip3-concurrent-outbound-http-calls/.gitignore new file mode 100644 index 0000000..386474f --- /dev/null +++ b/examples/wasip3-concurrent-outbound-http-calls/.gitignore @@ -0,0 +1,2 @@ +target/ +.spin/ diff --git a/examples/wasip3-concurrent-outbound-http-calls/Cargo.lock b/examples/wasip3-concurrent-outbound-http-calls/Cargo.lock new file mode 100644 index 0000000..ab2e228 --- /dev/null +++ b/examples/wasip3-concurrent-outbound-http-calls/Cargo.lock @@ -0,0 +1,1368 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "concurrent-outbound-http-calls" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures", + "http", + "spin-sdk", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "find-msvc-tools" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[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.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "postgres-protocol" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbef655056b916eb868048276cfd5d6a7dea4f81560dfd047f97c8c6fe3fcfd4" +dependencies = [ + "base64", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4605b7c057056dd35baeb6ac0c0338e4975b1f2bef0f65da953285eb007095" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + +[[package]] +name = "postgres_range" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6dce28dc5ba143d8eb157b62aac01ae5a1c585c40792158b720e86a87642101" +dependencies = [ + "postgres-protocol", + "postgres-types", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.107", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "routefinder" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0971d3c8943a6267d6bd0d782fdc4afa7593e7381a92a3df950ff58897e066b5" +dependencies = [ + "smartcow", + "smartstring", +] + +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec", + "num-traits", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smartcow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "656fcb1c1fca8c4655372134ce87d8afdf5ec5949ebabe8d314be0141d8b5da2" +dependencies = [ + "smartstring", +] + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "spin-executor" +version = "5.0.0" +dependencies = [ + "futures", + "once_cell", + "wasi", +] + +[[package]] +name = "spin-macro" +version = "5.0.0" +dependencies = [ + "anyhow", + "bytes", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "spin-sdk" +version = "5.0.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "form_urlencoded", + "futures", + "http", + "once_cell", + "postgres_range", + "routefinder", + "rust_decimal", + "serde", + "serde_json", + "spin-executor", + "spin-macro", + "spin-wasip3-http", + "spin-wasip3-http-macro", + "thiserror", + "uuid", + "wasi", + "wit-bindgen 0.43.0", +] + +[[package]] +name = "spin-wasip3-http" +version = "5.0.0" +dependencies = [ + "anyhow", + "bytes", + "http", + "http-body", + "http-body-util", + "wasip3", +] + +[[package]] +name = "spin-wasip3-http-macro" +version = "5.0.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a26dbd934e5451d21ef060c018dae56fc073894c5a7896f882928a76e6d081b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.13.1+wasi-0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f43d1c36145feb89a3e61aa0ba3e582d976a8ab77f1474aa0adb80800fe0cf8" +dependencies = [ + "wit-bindgen-rt 0.24.0", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.2.1+wasi-0.3.0-rc-2025-09-16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb2796323e2357ae2d4ba2b781a0392b533f40a5b9f534eef49b23e54186d64" +dependencies = [ + "bytes", + "http", + "http-body", + "thiserror", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.107", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a" +dependencies = [ + "leb128fmt", + "wasmparser 0.235.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser 0.239.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b055604ba04189d54b8c0ab2c2fc98848f208e103882d5c0b984f045d5ea4d20" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.235.0", + "wasmparser 0.235.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.239.0", + "wasmparser 0.239.0", +] + +[[package]] +name = "wasmparser" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161296c618fa2d63f6ed5fffd1112937e803cb9ec71b32b01a76321555660917" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a18712ff1ec5bd09da500fe1e91dec11256b310da0ff33f8b4ec92b927cf0c6" +dependencies = [ + "wit-bindgen-rt 0.43.0", + "wit-bindgen-rust-macro 0.43.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro 0.46.0", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c53468e077362201de11999c85c07c36e12048a990a3e0d69da2bd61da355d0" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.235.0", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.239.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0780cf7046630ed70f689a098cd8d56c5c3b22f2a7379bbdb088879963ff96" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd734226eac1fd7c450956964e3a9094c9cee65e9dafdf126feef8c0096db65" +dependencies = [ + "bitflags", + "futures", + "once_cell", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531ebfcec48e56473805285febdb450e270fa75b2dacb92816861d0473b4c15f" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.107", + "wasm-metadata 0.235.0", + "wit-bindgen-core 0.43.0", + "wit-component 0.235.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.107", + "wasm-metadata 0.239.0", + "wit-bindgen-core 0.46.0", + "wit-component 0.239.0", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7852bf8a9d1ea80884d26b864ddebd7b0c7636697c6ca10f4c6c93945e023966" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.107", + "wit-bindgen-core 0.43.0", + "wit-bindgen-rust 0.43.0", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.107", + "wit-bindgen-core 0.46.0", + "wit-bindgen-rust 0.46.0", +] + +[[package]] +name = "wit-component" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a57a11109cc553396f89f3a38a158a97d0b1adaec113bd73e0f64d30fb601f" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.235.0", + "wasm-metadata 0.235.0", + "wasmparser 0.235.0", + "wit-parser 0.235.0", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.239.0", + "wasm-metadata 0.239.0", + "wasmparser 0.239.0", + "wit-parser 0.239.0", +] + +[[package]] +name = "wit-parser" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1f95a87d03a33e259af286b857a95911eb46236a0f726cbaec1227b3dfc67a" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.235.0", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.239.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.107", +] diff --git a/examples/wasip3-concurrent-outbound-http-calls/Cargo.toml b/examples/wasip3-concurrent-outbound-http-calls/Cargo.toml new file mode 100644 index 0000000..28bc248 --- /dev/null +++ b/examples/wasip3-concurrent-outbound-http-calls/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "concurrent-outbound-http-calls" +authors = ["itowlson "] +description = "" +version = "0.1.0" +rust-version = "1.90" # required for `wasm32-wasip2` target to build WASIp3 +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +futures = "0.3.31" +http = "1.3" +spin-sdk = { path = "../..", features = ["wasip3-unstable"] } + +[workspace] diff --git a/examples/wasip3-concurrent-outbound-http-calls/spin.toml b/examples/wasip3-concurrent-outbound-http-calls/spin.toml new file mode 100644 index 0000000..6d7c943 --- /dev/null +++ b/examples/wasip3-concurrent-outbound-http-calls/spin.toml @@ -0,0 +1,20 @@ +#:schema https://schemas.spinframework.dev/spin/manifest-v2/latest.json + +spin_manifest_version = 2 + +[application] +name = "concurrent-outbound-http-calls" +version = "0.1.0" +authors = ["The Spin project"] +description = "Demonstrates making concurrent outbound HTTP calls in WASIp3" + +[[trigger.http]] +route = "/..." +component = "concurrent-outbound-http-calls" + +[component.concurrent-outbound-http-calls] +source = "target/wasm32-wasip2/release/concurrent_outbound_http_calls.wasm" +allowed_outbound_hosts = ["https://spinframework.dev", "https://component-model.bytecodealliance.org/"] +[component.concurrent-outbound-http-calls.build] +command = "cargo build --target wasm32-wasip2 --release" +watch = ["src/**/*.rs", "Cargo.toml"] diff --git a/examples/wasip3-concurrent-outbound-http-calls/src/lib.rs b/examples/wasip3-concurrent-outbound-http-calls/src/lib.rs new file mode 100644 index 0000000..aa48164 --- /dev/null +++ b/examples/wasip3-concurrent-outbound-http-calls/src/lib.rs @@ -0,0 +1,30 @@ +use std::pin::pin; + +use futures::future::Either; +use http::Request; +use spin_sdk::http_wasip3::{send, EmptyBody, IntoResponse}; +use spin_sdk::http_wasip3::http_component; + +#[http_component] +async fn handle_concurrent_outbound_http_calls(_req: spin_sdk::http_wasip3::Request) -> anyhow::Result { + + let spin = pin!(get_content_length("https://spinframework.dev")); + let book = pin!(get_content_length("https://component-model.bytecodealliance.org/")); + + let (first, len) = match futures::future::select(spin, book).await { + Either::Left(len) => ("Spin docs", len), + Either::Right(len) => ("Component model book", len), + }; + + let response = format!("{first} site was first response with content-length {:?}\n", len.0?); + + Ok(response) +} + +async fn get_content_length(url: &str) -> anyhow::Result> { + let request = Request::get(url).body(EmptyBody::new())?; + let response = send(request).await?; + let cl_header = response.headers().get("content-length"); + let cl = cl_header.and_then(|hval| hval.to_str().ok()).and_then(|hval| hval.parse().ok()); + Ok(cl) +} From 0aa016573f7d4d76133b93308c9c3417a9c879ae Mon Sep 17 00:00:00 2001 From: Brian Hardock Date: Fri, 24 Oct 2025 12:55:59 -0600 Subject: [PATCH 5/5] Rename http_component to http_service and use unstable executor Signed-off-by: Brian Hardock --- crates/spin-wasip3-http-macro/src/lib.rs | 8 ++++---- examples/wasip3-concurrent-outbound-http-calls/spin.toml | 1 + examples/wasip3-concurrent-outbound-http-calls/src/lib.rs | 4 ++-- examples/wasip3-http-axum-router/spin.toml | 1 + examples/wasip3-http-axum-router/src/lib.rs | 6 +++--- examples/wasip3-http-hello-world/spin.toml | 1 + examples/wasip3-http-hello-world/src/lib.rs | 4 ++-- examples/wasip3-http-send-request/spin.toml | 1 + examples/wasip3-http-send-request/src/lib.rs | 4 ++-- 9 files changed, 17 insertions(+), 13 deletions(-) diff --git a/crates/spin-wasip3-http-macro/src/lib.rs b/crates/spin-wasip3-http-macro/src/lib.rs index e4ad70c..448797f 100644 --- a/crates/spin-wasip3-http-macro/src/lib.rs +++ b/crates/spin-wasip3-http-macro/src/lib.rs @@ -3,7 +3,7 @@ use quote::quote; /// Marks an `async fn` as an HTTP component entrypoint for Spin. /// -/// The `#[http_component]` attribute designates an asynchronous function as the +/// The `#[http_service]` attribute designates an asynchronous function as the /// handler for incoming HTTP requests in a Spin component using the WASI Preview 3 /// (`wasip3`) HTTP ABI. /// @@ -25,9 +25,9 @@ use quote::quote; /// # Example /// /// ```ignore -/// use spin_sdk::http_wasip3::{http_component, Request, IntoResponse}; +/// use spin_sdk::http_wasip3::{http_service, Request, IntoResponse}; /// -/// #[http_component] +/// #[http_service] /// async fn my_handler(request: Request) -> impl IntoResponse { /// // Your logic goes here /// } @@ -40,7 +40,7 @@ use quote::quote; /// handler’s entrypoint. This allows the function to be invoked automatically /// by the Spin runtime when HTTP requests are received. #[proc_macro_attribute] -pub fn http_component(_attr: TokenStream, item: TokenStream) -> TokenStream { +pub fn http_service(_attr: TokenStream, item: TokenStream) -> TokenStream { let func = syn::parse_macro_input!(item as syn::ItemFn); if func.sig.asyncness.is_none() { diff --git a/examples/wasip3-concurrent-outbound-http-calls/spin.toml b/examples/wasip3-concurrent-outbound-http-calls/spin.toml index 6d7c943..b33beef 100644 --- a/examples/wasip3-concurrent-outbound-http-calls/spin.toml +++ b/examples/wasip3-concurrent-outbound-http-calls/spin.toml @@ -11,6 +11,7 @@ description = "Demonstrates making concurrent outbound HTTP calls in WASIp3" [[trigger.http]] route = "/..." component = "concurrent-outbound-http-calls" +executor = { type = "wasip3-unstable" } [component.concurrent-outbound-http-calls] source = "target/wasm32-wasip2/release/concurrent_outbound_http_calls.wasm" diff --git a/examples/wasip3-concurrent-outbound-http-calls/src/lib.rs b/examples/wasip3-concurrent-outbound-http-calls/src/lib.rs index aa48164..46be3aa 100644 --- a/examples/wasip3-concurrent-outbound-http-calls/src/lib.rs +++ b/examples/wasip3-concurrent-outbound-http-calls/src/lib.rs @@ -3,9 +3,9 @@ use std::pin::pin; use futures::future::Either; use http::Request; use spin_sdk::http_wasip3::{send, EmptyBody, IntoResponse}; -use spin_sdk::http_wasip3::http_component; +use spin_sdk::http_wasip3::http_service; -#[http_component] +#[http_service] async fn handle_concurrent_outbound_http_calls(_req: spin_sdk::http_wasip3::Request) -> anyhow::Result { let spin = pin!(get_content_length("https://spinframework.dev")); diff --git a/examples/wasip3-http-axum-router/spin.toml b/examples/wasip3-http-axum-router/spin.toml index c142586..aaa34c2 100644 --- a/examples/wasip3-http-axum-router/spin.toml +++ b/examples/wasip3-http-axum-router/spin.toml @@ -11,6 +11,7 @@ description = "An example application using axum" [[trigger.http]] route = "/..." component = "axum-router" +executor = { type = "wasip3-unstable" } [component.axum-router] source = "../../target/wasm32-wasip2/release/axum_router.wasm" diff --git a/examples/wasip3-http-axum-router/src/lib.rs b/examples/wasip3-http-axum-router/src/lib.rs index 463da17..869ca1e 100644 --- a/examples/wasip3-http-axum-router/src/lib.rs +++ b/examples/wasip3-http-axum-router/src/lib.rs @@ -4,11 +4,11 @@ use axum::{ Json, Router, }; use serde::{Deserialize, Serialize}; -use spin_sdk::http_wasip3::{http_component, IntoResponse, Request}; +use spin_sdk::http_wasip3::{http_service, IntoResponse, Request}; use tower_service::Service; -/// Sends a request to a URL. -#[http_component] +/// Demonstrates integration with the Axum web framework +#[http_service] async fn handler(req: Request) -> impl IntoResponse { Router::new() .route("/", get(root)) diff --git a/examples/wasip3-http-hello-world/spin.toml b/examples/wasip3-http-hello-world/spin.toml index 9d44a64..67daf8d 100644 --- a/examples/wasip3-http-hello-world/spin.toml +++ b/examples/wasip3-http-hello-world/spin.toml @@ -11,6 +11,7 @@ version = "1.0.0" [[trigger.http]] route = "/hello" component = "hello" +executor = { type = "wasip3-unstable" } [component.hello] source = "../../target/wasm32-wasip2/release/wasip3_hello_world.wasm" diff --git a/examples/wasip3-http-hello-world/src/lib.rs b/examples/wasip3-http-hello-world/src/lib.rs index 7aa8193..e49e10e 100644 --- a/examples/wasip3-http-hello-world/src/lib.rs +++ b/examples/wasip3-http-hello-world/src/lib.rs @@ -1,7 +1,7 @@ -use spin_sdk::http_wasip3::{http_component, Request}; +use spin_sdk::http_wasip3::{http_service, Request}; /// A simple Spin HTTP component. -#[http_component] +#[http_service] async fn hello_world(_req: Request) -> &'static str { "Hello, world!" } diff --git a/examples/wasip3-http-send-request/spin.toml b/examples/wasip3-http-send-request/spin.toml index 6175700..e9b3810 100644 --- a/examples/wasip3-http-send-request/spin.toml +++ b/examples/wasip3-http-send-request/spin.toml @@ -11,6 +11,7 @@ version = "1.0.0" [[trigger.http]] route = "/..." component = "send" +executor = { type = "wasip3-unstable" } [component.send] source = "../../target/wasm32-wasip2/release/send_request.wasm" diff --git a/examples/wasip3-http-send-request/src/lib.rs b/examples/wasip3-http-send-request/src/lib.rs index 331caaf..79cf9bb 100644 --- a/examples/wasip3-http-send-request/src/lib.rs +++ b/examples/wasip3-http-send-request/src/lib.rs @@ -1,7 +1,7 @@ -use spin_sdk::http_wasip3::{http_component, send, EmptyBody, IntoResponse, Request, Result}; +use spin_sdk::http_wasip3::{http_service, send, EmptyBody, IntoResponse, Request, Result}; /// Sends a request to a URL. -#[http_component] +#[http_service] async fn send_request(_req: Request) -> Result { let outgoing = http::Request::get("https://bytecodealliance.org").body(EmptyBody::new())?;