WebAssemblyコンポーネントでHTTPハンドラーを作成

WASI Preview 2 の ```wasi:http``` を用いた HTTP ハンドラー処理を WebAssembly コンポーネントで実装してみました。

今回も「[cargo componentでWASI Preview 2を使ったコンポーネントを作成](https://qiita.com/fits/items/aaf477de275573658f8a)」と同様の方法を用います。

# コンポーネント作成

HTTP ハンドラーの wit は次のようにするだけでした。

* ```wasi:http/incoming-handler``` を export する

実際の wit ファイルはこのようになります。

```js:wit/world.wit
package component:http-sample;

world sample {
    export wasi:http/incoming-handler@0.2.0;
}
```

[WASI](https://github.com/WebAssembly/WASI) の wit をローカル参照する方法で wasi:http を使う場合の依存設定はこのようになりました。（wasi:cli を利用する際の依存設定へ wasi:http を加えるだけ）

```toml:Cargo.toml
[package]
name = "http_sample"
version = "0.1.0"
edition = "2021"

[dependencies]
bitflags = "2.5.0"
wit-bindgen-rt = "0.24.0"

[lib]
crate-type = ["cdylib"]

[package.metadata.component]
package = "component:http-sample"

[package.metadata.component.dependencies]

[package.metadata.component.target.dependencies]
"wasi:http" = { path = "./WASI/preview2/http" }
"wasi:cli" = { path = "./WASI/preview2/cli" }
"wasi:clocks" = { path = "./WASI/preview2/clocks" }
"wasi:filesystem" = { path = "./WASI/preview2/filesystem" }
"wasi:io" = { path = "./WASI/preview2/io" }
"wasi:random" = { path = "./WASI/preview2/random" }
"wasi:sockets" = { path = "./WASI/preview2/sockets" }
```

実装はこのようにしてみました。
```ok:<URLパス>```　という文字列をレスポンスとして返すだけです。

```content-length``` レスポンスヘッダーを設定しておかないと、[wasmtime](https://github.com/bytecodealliance/wasmtime) の処理でエラーとなったのでご注意ください。

```rust:src/lib.rs
#[allow(warnings)]
mod bindings;

use bindings::exports::wasi::http::incoming_handler::Guest;
use bindings::wasi::http::types::{ErrorCode, HeaderError, Headers, OutgoingResponse, ResponseOutparam};
use bindings::wasi::io::streams::StreamError;

struct Component;

impl Guest for Component {
    fn handle(
        request: bindings::exports::wasi::http::incoming_handler::IncomingRequest,
        response_out: bindings::exports::wasi::http::incoming_handler::ResponseOutparam,
    ) {
        let s = format!("ok:{}", request.path_with_query().unwrap_or_default());

        ResponseOutparam::set(response_out, create_response(s));
    }
}

fn create_response(s: String) -> Result<OutgoingResponse, ErrorCode> {
    let c = s.as_bytes();

    let h = Headers::new();
    h.append(&"content-length".to_string(), &c.len().to_string().into())?;

    let r = OutgoingResponse::new(h);

    let b = r.body()?;
    let w = b.write()?;
    // レスポンスボディの書き込み
    w.write(c)?;
    w.flush()?;

    Ok(r)
}

impl From<()> for ErrorCode {
    fn from(_value: ()) -> Self {
        ErrorCode::InternalError(None)
    }
}

impl From<StreamError> for ErrorCode {
    fn from(value: StreamError) -> Self {
        ErrorCode::InternalError(Some(value.to_string()))
    }
}

impl From<HeaderError> for ErrorCode {
    fn from(value: HeaderError) -> Self {
        ErrorCode::InternalError(Some(value.to_string()))
    }
}

bindings::export!(Component with_types_in bindings);
```

# ビルドと実行

wasm32-unknown-unknown でビルドします。

```sh:ビルド
$ cargo component build --target wasm32-unknown-unknown --release
```

[wasmtime](https://github.com/bytecodealliance/wasmtime) の serve コマンドで実行します。
これで Web サーバーが立ち上がり、リクエストを WebAssembly コンポーネントへ中継してくれます。

```sh:実行
$ wasmtime serve target/wasm32-unknown-unknown/release/http_sample.wasm
Serving HTTP on http://0.0.0.0:8080/
```

アクセスしてみると、正常に動作している事を確認できました。

```sh:動作確認
$ curl -v http://localhost:8080/a/1

...省略
< HTTP/1.1 200 OK
< content-length: 7
< date: Sat, 27 Apr 2024 02:22:32 GMT
< 
* Connection #0 to host localhost left intact
ok:/a/1
```