diff --git a/Cargo.lock b/Cargo.lock index de05016..278b21d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,12 @@ version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "autocfg" version = "1.4.0" @@ -228,7 +234,7 @@ dependencies = [ "mime", "serde_json", "thiserror", - "wit-bindgen", + "wit-bindgen 0.46.0", ] [[package]] @@ -240,6 +246,15 @@ dependencies = [ "syn", ] +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -249,6 +264,14 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fetch" +version = "0.1.0" +dependencies = [ + "anyhow", + "wstd", +] + [[package]] name = "flate2" version = "1.1.0" @@ -328,6 +351,21 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -443,6 +481,29 @@ dependencies = [ "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 = "icu_collections" version = "1.5.0" @@ -617,6 +678,15 @@ dependencies = [ "serde", ] +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "1.0.15" @@ -734,6 +804,12 @@ version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1155,6 +1231,21 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen 0.51.0", +] + [[package]] name = "wasm-encoder" version = "0.239.0" @@ -1219,6 +1310,15 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "bitflags 2.9.0", +] + [[package]] name = "wit-bindgen-core" version = "0.46.0" @@ -1310,6 +1410,38 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 877933a..c57bdf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,7 @@ [workspace] members = ["derive", ".", "examples/*"] +# examples/fetch is a component-model/WASI-HTTP example built with cargo-component, not plain `cargo build` +exclude = [ "examples/fetch" ] [workspace.package] version = "0.3.4" diff --git a/examples/fetch/Cargo.toml b/examples/fetch/Cargo.toml new file mode 100644 index 0000000..6e94586 --- /dev/null +++ b/examples/fetch/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "fetch" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +anyhow = "1" + +[package.metadata.component] +package = "component:fetch" diff --git a/examples/fetch/README.md b/examples/fetch/README.md new file mode 100644 index 0000000..62ab766 --- /dev/null +++ b/examples/fetch/README.md @@ -0,0 +1,56 @@ +⏮️ Back to Rust [README.md](../../README.md) + +# Fetch + +A minimal example demonstrating outbound HTTP requests using the [WASI-HTTP](https://github.com/WebAssembly/wasi-http) interface via the [`wstd`](https://crates.io/crates/wstd) crate. + +Unlike the other HTTP examples that use the synchronous FastEdge SDK, this example uses the WASI component model with an **async** handler and a proper HTTP client (`wstd::http::Client`). + +## How it works + +The app receives an incoming request, reads the target URL from the `x-fetch-url` header, makes an outbound GET request to that URL, and streams the response back to the caller. + +If the `x-fetch-url` header is absent, it defaults to `https://httpbin.org/get`. + +## Request headers + +| Header | Required | Description | +|--------|----------|-------------| +| `x-fetch-url` | No | URL to fetch. Defaults to `https://httpbin.org/get` | + +## Example + +```bash +curl -H "x-fetch-url: https://httpbin.org/uuid" https:/// +``` + +## Build + +### Prerequisites + +- Rust toolchain +- [`cargo-component`](https://github.com/bytecodealliance/cargo-component) + +```bash +cargo install cargo-component +``` + +### Compile + +```bash +cargo component build --release +``` + +The compiled component will be at: +``` +target/wasm32-wasip1/release/fetch.wasm +``` + +## Key differences from FastEdge SDK examples + +| | FastEdge SDK | This example (WASI-HTTP) | +|---|---|---| +| Handler | `fn main(req)` — sync | `async fn main(req)` — async | +| Macro | `#[fastedge::http]` | `#[wstd::http_server]` | +| Outbound HTTP | `fastedge::send_request(req)` | `Client::new().send(req).await` | +| Build tool | `cargo build` | `cargo component build` | diff --git a/examples/fetch/src/lib.rs b/examples/fetch/src/lib.rs new file mode 100644 index 0000000..479c524 --- /dev/null +++ b/examples/fetch/src/lib.rs @@ -0,0 +1,43 @@ +/* +* Copyright 2025 G-Core Innovations SARL +*/ +/* +Example app demonstrating the WASI-HTTP interface via the wstd crate. + +The app receives an incoming HTTP request and makes an outbound HTTP request +to the URL specified in the `x-fetch-url` header (defaults to https://httpbin.org/get). + +Build with cargo-component: + cargo component build --release +*/ + +use anyhow::anyhow; +use wstd::http::body::Body; +use wstd::http::{Client, Request, Response}; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + let target_url = request + .headers() + .get("x-fetch-url") + .and_then(|v| v.to_str().ok()) + .unwrap_or("https://httpbin.org/get") + .to_string(); + + println!("Fetching: {target_url}"); + + let upstream_req = Request::get(&target_url) + .header("accept", "application/json") + .body(Body::empty()) + .map_err(|e| anyhow!("failed to build request: {e}"))?; + + let client = Client::new(); + let response = client + .send(upstream_req) + .await + .map_err(|e| anyhow!("request failed: {e}"))?; + + println!("Response status: {}", response.status()); + + Ok(response) +} diff --git a/src/lib.rs b/src/lib.rs index d569fec..5972382 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,9 +15,14 @@ //! * **ProxyWasm API**: Compatibility layer for [ProxyWasm] environments (Envoy, etc.). //! Available via the [`fastedge::proxywasm`](`proxywasm`) module when the `proxywasm` feature is enabled. //! +//! * **WASI-HTTP interface**: An alternative to the FastEdge SDK using the standard [WASI-HTTP] +//! interface via the [`wstd`] crate. +//! //! [WIT]: https://component-model.bytecodealliance.org/design/wit.html //! [WebAssembly components]: https://component-model.bytecodealliance.org //! [ProxyWasm]: https://github.com/proxy-wasm/spec +//! [WASI-HTTP]: https://github.com/WebAssembly/wasi-http +//! [`wstd`]: https://crates.io/crates/wstd //! //! ## Features //! @@ -64,6 +69,39 @@ //! cargo build --target wasm32-wasip1 --release //! ``` //! +//! ### Using the WASI-HTTP Interface (async, `wstd`) +//! +//! As an alternative to the synchronous FastEdge SDK, you can use the standard [WASI-HTTP] +//! interface via the [`wstd`] crate. This enables an async handler and a proper HTTP client: +//! +//! ```no_run +//! use wstd::http::body::Body; +//! use wstd::http::{Client, Request, Response}; +//! +//! #[wstd::http_server] +//! async fn main(_request: Request) -> anyhow::Result> { +//! let upstream_req = Request::get("https://api.example.com/data") +//! .header("accept", "application/json") +//! .body(Body::empty())?; +//! +//! let response = Client::new().send(upstream_req).await?; +//! Ok(response) +//! } +//! ``` +//! +//! Build with [`cargo-component`] instead of `cargo build`: +//! +//! ```bash +//! cargo install cargo-component +//! cargo component build --release +//! ``` +//! +//! See the [fetch example] for a complete working app and a side-by-side comparison +//! with the FastEdge SDK approach. +//! +//! [`cargo-component`]: https://github.com/bytecodealliance/cargo-component +//! [fetch example]: https://github.com/G-Core/FastEdge-sdk-rust/tree/main/examples/fetch +//! //! ## Feature Flags //! //! - `proxywasm` (default): Enable ProxyWasm compatibility layer