Backend CanisterでHTTP応答を返す (Rust)
はじめに
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リクエストを処理できるようserde
、serde_bytes
を追加します。
また、HTTP応答をJSON形式で返すことを想定しserde_json
も追加します。
$ cargo add serde serde_bytes serde_json
コマンドを実行した後の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が出力されています。
{
"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が公開している内容を参考にするとよいでしょう。
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;
}
参考情報
src/hello_backend/src/lib.rs編集
プログラムは長いですが、独自に実装している箇所はData
構造体と、queryメソッドのhttp_request
のみです。
それ以外のHttpRequest
とHttpResponse
に関連する定義は、Dfinityが公開しているhttp_gateway.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,
},
}
}
参考情報
データ型定義
HTTPリクエスト処理
ビルド・デプロイ
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>
は環境に合わせて指定してください。
- http://<Canister Id>.localhost:4943/
- http://127.0.0.1:4943/?canisterId=<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