RustでWebAssembly Componentを作る - HTTPリクエストを送る
※WebAssembly 勉強中です。この記事は間違った情報や不必要な手順を含む可能性があります。
WebAssembly Component(以下、wasm component)の環境設定やwasm32-wasip2ビルドは可能になっている前提です。その辺りは前回記事にあります
前回:RustでWebAssembly Componentを作る - cliでHello Worldする
この記事のコード例
wasm componentからHTTPリクエストを送る
RustのアプリケーションからHTTPリクエストを送る際はreqwestといったクレートが良く利用されるが、wasm componentの場合はwasi:httpインターフェースを利用することになる。
wasm componentから外部へHTTPリクエストを送信する処理は、wasi:httpのoutgoing-handlerを利用する。tokioといった非同期ランタイムも入れられないので通常のRustアプリケーションと異なる部分が多い。
余談:wasm componentで非同期にしたい処理を書くのは結構辛かったが、wstdの登場でだいぶやりやすくなった。wasm componentがfutureやstreamに対応するwasi3以降はもっと良くなる可能性もあり、2025年の9月時点では過渡期な感じはある。
プロジェクトセットアップ
前回記事のようにセットアップしてwasi:httpを追加で導入する。まずはhello world
Cargo.toml
[package]
name = "http-sample"
version = "0.1.0"
edition = "2024"
[dependencies]
wit-bindgen-rt = { version = "0.44.0", features = ["bitflags"] }
[lib]
crate-type = ["cdylib"]
[package.metadata.component]
package = "component:http"
[package.metadata.component.dependencies]
[package.metadata.component.target.dependencies]
"wasi:http" = "0.2.7"
"wasi:io" = "0.2.7"
wit
package component:http;
world example {
export sample-request: func() -> result<string,string>;
}
lib.rs
src/lib.rs
#[allow(warnings)]
mod bindings;
use bindings::Guest;
struct Component;
impl Guest for Component {
fn sample_request() -> Result<String, String> {
Ok("Hello from Rust!".to_string())
}
}
bindings::export!(Component with_types_in bindings);
実行する
cargo build -r --target wasm32-wasip2
wasmtime run --invoke 'sample-request()' target/wasm32-wasip2/release/http_sample.wasm
# 出力
# ok("Hello from Rust!")
エラーなく出力されればセットアップは終了
HTTPリクエストを送信する
https://httpbin.org/get にリクエストを送信してurlを取得し、返却するコードを実装する
curl https://httpbin.org/get
# レスポンス例
# {
# "args": {},
# "headers": {},
# "origin": "",
# "url": "https://httpbin.org/get"
# }
依存関係
wasm-component環境に対応した非同期ランタイムにwstdがある。outgoing-handlerの利用がかなり簡易になるので利用する。将来的にはtokioなどがwasi0.2をサポートしたら置き換えると書かれてはいるが、いつになるかはわからない。
cargo add anyhow serde serde_json wasi wstd
HTTPリクエストする関数を作る
use anyhow::{bail, Context};
use serde::Deserialize;
use wstd::{http::{Client, Request}, io};
#[derive(Deserialize)]
struct HttpBinResponse {
url: String,
}
async fn get_httpbin() -> anyhow::Result<String> {
let url = "https://httpbin.org/get";
let request = Request::get(url).body(io::empty())?;
let mut response = Client::new().send(request).await?;
if response.status() != 200 {
bail!("Unexpected status code: {}", response.status());
}
let body = response.body_mut().bytes().await?;
let result: HttpBinResponse = serde_json::from_slice(&body).with_context(|| {
let preview = String::from_utf8_lossy(&body);
format!("Failed to parse JSON. Body: {preview}")
})?;
Ok(result.url)
}
Guestで使う
上記のget_httpbinを利用する。非同期関数が利用できるようにblock_onで包む。
impl Guest for Component {
fn sample_request() -> Result<String, String> {
let result = block_on(async {
get_httpbin().await
});
match result {
Ok(url) => Ok(url),
Err(e) => {
let msg = format!("Error occurred: {}", e);
Err(msg)
}
}
}
}
動作確認
wasi:httpに依存したのでwasmtimeコマンドで、-S httpを追加する必要がある
cargo build -r --target wasm32-wasip2
wasmtime run -S http --invoke 'sample-request()' target/wasm32-wasip2/release/http_sample.wasm
# ok("https://httpbin.org/get")
urlを存在しないものに変えて実行するとエラーとなる
err("Error occurred: Unexpected status code: 404 Not Found")
ということでHTTPリクエストを送信可能になった。
wstdを利用しないケース
outgoing_handlerとwasi::io::pollを利用する。勿論、wstdも内部でそれらを使っている。コード例は下記のブログなどに記載がある。個人的にはoutgoing_handlerを直接使うのは厳しさがあった。
“Designing an Async Runtime for WASI 0.2 — 2024-02-29”
とても難しかった
wasm componentを学び始めてHTTPリクエストを送りたいと考えてから、上記のような実装に至るまでとても時間がかかった。outgoing_handlerとpollに頭を悩ませていたタイミングで、2025年8月wstdがbytecodeallianceで移管され、存在に気づけたのはとても有り難く、グッと楽になった印象です。
次の記事ではRustのアプリケーションでwasmを読み込んで利用するところを書く予定です