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を読み込んで利用するところを書く予定です