♾️

Backend CanisterでHTTP応答を返す (Rust)

2024/05/13に公開

はじめに

Internet Computer(IC)と呼ばれる分散クラウド環境で動作するCanisterをRust言語で開発するために調査した内容をまとめています。

前回の『はじめてのCanister開発 (Rust)』では、dfx newコマンドを実行してBackend CanisterとFrontend Canisterが生成されることを解説しました。生成されたHelloサンプルでは、Backend CanisterにCandidと呼ばれるIDL定義に対応した公開インタフェースを用意し、静的Webホスティングとして機能するFrontend Canister(Asset Canister)に配置したHTMLコンテンツからJavaScript経由で呼び出しています。Backend Canisterとの通信には、CBORエンコードされECDSAやEd25519による認証も含めたIC独自のデータ形式が使われており、JavaScript(agent-js)以外にも、Java(icj4-agent)やPython(ic-py)など主要な言語でライブラリが提供されています。

本記事では、前述の呼び出し方法とは別に、Backend CanisterでHTTPリクエストを受け付られるようにする方法について解説します。
たとえば、IC上にAPIサーバーを公開するといったことも実現できるかと思います。

プロジェクトの作成

ターミナル上でdfx new <プロジェクト名>と入力して実行すると、Backend開発に使用するプログラミング言語やFrontendのフレームワークなどについて対話形式で訊かれた後、Helloサンプルが生成されます。

<プロジェクト名>には作成するプロジェクトの名称を指定します。例えば、プロジェクト名を『hello』にする場合、以下のコマンドを実行してください。

$ dfx new hello

Backendのプログラミング言語の選択

Backend Canisterの開発に使用するプログラミング言語を訊いてきますので、『Rust』を選択します。

? Select a backend language: ›  
  Motoko
❯ Rust
  TypeScript (Azle)
  Python (Kybra)

Frontend frameworkの選択

本解説ではFrontend Canisterは不要です。

$ dfx new hello
✔ Select a backend language: · Rust
? Select a frontend framework: ›  
  SvelteKit
  React
  Vue
  Vanilla JS
  No JS template
❯ No frontend canister

Extra featuresの選択

本解説では不要です。

? Add extra features (space to select, enter to confirm) ›
⬚ Internet Identity
⬚ Bitcoin (Regtest)

ディレクトリ構成

dfxコマンドが完了すると、hello(プロジェクト名)のディレクトリが作成されてファイルが生成されます。

hello
├── .git
│    ︙
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── README.md
├── dfx.json
└── src
    └── hello_backend
        ├── Cargo.toml
        ├── hello_backend.did
        └── src
            └── lib.rs

Local Canister実行環境の起動

開発時はIC上ではなくローカルPC上にデプロイしてデバッグ等を行うとよいでしょう。
あらかじめ、dfx startコマンドでLocal Canister実行環境を起動しておきます。

$ cd hello
$ dfx start --background --clean

dependenciesの追加

CanisterがHTTPリクエストを処理できるようserdeserde_bytesを追加します。
また、HTTP応答をJSON形式で返すことを想定しserde_jsonも追加します。

$ cargo add serde serde_bytes serde_json

コマンドを実行した後のCargo.tomlは概ね以下のよう内容になります。

src/hello_backend/Cargo.toml
[package]
name = "hello_backend"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

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

[dependencies]
candid = "0.10"
ic-cdk = "0.13"
ic-cdk-timers = "0.7" # Feel free to remove this dependency if you don't need timers
serde = "1.0.201"
serde_bytes = "0.11.14"
serde_json = "1.0.117"

dfx.jsonの編集

以下のようなdfx.jsonが出力されています。

dfx.json
{
  "canisters": {
    "hello_backend": {
      "candid": "src/hello_backend/hello_backend.did",
      "package": "hello_backend",
      "type": "rust"
    }
  },
  "defaults": {
    "build": {
      "args": "",
      "packtool": ""
    }
  },
  "output_env_file": ".env",
  "version": 1
}

必須ではありませんが、hello_backend項目に以下のエントリを追加しておくと、dfx deployコマンドの実行結果に、Frontend CanisterへのURLが表示されるようになります。

      "frontend": {},

src/hello_backend/hello_backend.didの編集

HTTP GETリクエストを受け付けるhttp_requestというqueryメソッドを定義します。
ICのBoundary Nodeを経由して呼び出されますので、そのI/Fに合わせる必要があります。Dfinityが公開している内容を参考にするとよいでしょう。

src/hello_backend/hello_backend.did
type HeaderField = record {
    text;
    text;
};

type HttpRequest = record {
    method: text;
    url: text;
    headers: vec HeaderField;
    body: blob;
    certificate_version: opt nat16;
};

type HttpResponse = record {
    status_code: nat16;
    headers: vec HeaderField;
    body: blob;
    upgrade : opt bool;
    streaming_strategy: opt StreamingStrategy;
};

type StreamingCallbackHttpResponse = record {
    body: blob;
    token: opt Token;
};

type Token = record {};

type StreamingStrategy = variant {
    Callback: record {
        callback: func (Token) -> (StreamingCallbackHttpResponse) query;
        token: Token;
    };
};

service : {
    http_request: (request: HttpRequest) -> (HttpResponse) query;
}

参考情報

https://github.com/dfinity/internet-identity/blob/release-2024-04-26/src/internet_identity/internet_identity.did

src/hello_backend/src/lib.rs編集

プログラムは長いですが、独自に実装している箇所はData構造体と、queryメソッドのhttp_requestのみです。
それ以外のHttpRequestHttpResponseに関連する定義は、Dfinityが公開しているhttp_gateway.rsからのコピペです。

src/hello_backend/src/lib.rs
use ic_cdk::query;
use candid::{define_function, CandidType};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;

pub type HeaderField = (String, String);

#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct Token {}

define_function!(pub StreamingCallbackFunction : (Token) -> (StreamingCallbackHttpResponse) query);

#[derive(Clone, Debug, CandidType, Deserialize)]
pub enum StreamingStrategy {
    Callback {
        callback: StreamingCallbackFunction,
        token: Token,
    },
}

#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct StreamingCallbackHttpResponse {
    pub body: ByteBuf,
    pub token: Option<Token>,
}

#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct HttpRequest {
    pub method: String,
    pub url: String,
    pub headers: Vec<HeaderField>,
    pub body: ByteBuf,
    pub certificate_version: Option<u16>,
}

#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct HttpResponse {
    pub status_code: u16,
    pub headers: Vec<HeaderField>,
    pub body: ByteBuf,
    pub upgrade: Option<bool>,
    pub streaming_strategy: Option<StreamingStrategy>,
}

// Data
#[derive(Serialize, Debug)]
struct Data {
    message: String,
}

#[query]
pub fn http_request(req: HttpRequest) -> HttpResponse {
    let parts: Vec<&str> = req.url.split('?').collect();
    match parts[0] {
        "/" => {
            // Data
            let data = Data {
                message: String::from("Hello, world"),
            };
            let body = serde_json::to_string(&data).unwrap();

            HttpResponse {
                status_code: 200,
                headers: vec![
                    (
                        "Content-Type".to_string(),
                        "application/json".to_string(),
                    ),
                    (
                        "Content-Length".to_string(),
                        body.len().to_string()
                    ),
                ],
                body: ByteBuf::from(body),
                upgrade: None,
                streaming_strategy: None,
            }
        },
        &_ => HttpResponse {
            status_code: 404,
            headers: vec![],
            body: ByteBuf::from(""),
            upgrade: None,
            streaming_strategy: None,
        },
    }
}

参考情報

データ型定義

https://github.com/dfinity/internet-identity/blob/release-2024-04-26/src/internet_identity_interface/src/http_gateway.rs

HTTPリクエスト処理

https://github.com/dfinity/internet-identity/blob/release-2024-04-26/src/internet_identity/src/http.rs

ビルド・デプロイ

dfx deployコマンドを実行します。ローカルPC上で動作確認を行います。
本番環境にデプロイする場合には--network=icを指定しますが、本記事では取り扱いません。

$ dfx deploy
Deploying all canisters.
Creating canisters...
Creating canister hello_backend...
︙
Installing code for canister hello_backend, with canister ID bkyz2-fmaaa-aaaaa-qaaaq-cai
Deployed canisters.
URLs:
  Frontend canister via browser
    hello_backend:
      - http://127.0.0.1:4943/?canisterId=bkyz2-fmaaa-aaaaa-qaaaq-cai
      - http://bkyz2-fmaaa-aaaaa-qaaaq-cai.localhost:4943/
  Backend canister via Candid interface:
    hello_backend: http://127.0.0.1:4943/?canisterId=bd3sg-teaaa-aaaaa-qaaba-cai&id=bkyz2-fmaaa-aaaaa-qaaaq-cai

※環境によってCanister Idの値は異なります。

Backend CanisterへのHTTPリクエスト

Web Browserからのアクセス

Web browserから以下のURLにアクセスします。 <Canister Id>は環境に合わせて指定してください。

curlからアクセス

$ curl http://bkyz2-fmaaa-aaaaa-qaaaq-cai.localhost:4943/
{"message":"Hello, world"}

注意事項

本番環境へデプロイした場合、以下の記載によれば、Boundary Nodesでコンテンツが一定時間キャッシュされるとのことですのでご注意ください。(おそらく、GETリクエストパスの単位では)

https://wiki.internetcomputer.org/wiki/Boundary_Nodes

Caching
To improve the user-perceived performance of the dapps hosted on the IC, >the boundary nodes currently provide response for HTTP assets. Responses >to requests are cached for 10s.

Discussion