Contents
Actix Web v4 のハンドラ基本構文
Actix Web v4 では、非同期ハンドラを async fn として定義し、戻り値に impl Responder を指定します。この書き方は最もシンプルでありながら型安全で、テストもしやすくなります。本節では「シンプルなハンドラの書き方」と「独自抽出器を作るための FromRequest 実装」の2つのポイントを中心に解説します。
async fn と impl Responder のシンプルな例
|
1 2 3 4 5 6 7 8 9 |
use actix_web::{get, HttpResponse, Responder}; /// GET /hello にマッピングされるハンドラ #[get("/hello")] async fn hello() -> impl Responder { // 文字列は自動で HttpResponse::Ok に変換されます "Hello, Actix Web v4!" } |
#[get(...)]マクロはルーティング情報を付与します。- 関数本体は普通の非同期コードなので、
awaitが必要な処理もそのまま書けます。 - 戻り値が文字列の場合、Actix の内部で
HttpResponse::Ok().body(..)に変換されます。
FromRequest トレイトの概要とカスタム抽出器の作り方
Actix は FromRequest を実装した型をハンドラ引数として受け取ります。標準では Path<T>, Query<T>, Json<T> が提供されていますが、独自ロジックが必要なときはカスタム抽出器を作成できます。
カスタム抽出器の実装例(エラーハンドリングを改善)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
use actix_web::{ dev::Payload, error::{ErrorBadRequest, ErrorInternalServerError}, FromRequest, HttpMessage, HttpRequest, }; use futures::future::{err, ok, Ready}; /// リクエストヘッダーから API キーを取得する抽出器 pub struct ApiKey(pub String); impl FromRequest for ApiKey { type Error = actix_web::Error; type Future = Ready<Result<Self, Self::Error>>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { // "x-api-key" ヘッダーが無ければ 400 エラーを返す match req.headers().get("x-api-key") { Some(value) => match value.to_str() { Ok(s) if !s.is_empty() => ok(ApiKey(s.to_owned())), Ok(_) => err(ErrorBadRequest( "The x‑api‑key header must not be empty", )), Err(_) => err(ErrorBadRequest( "Invalid UTF‑8 sequence in x‑api‑key header", )), }, None => err(ErrorBadRequest("Missing x-api-key header")), } } } |
to_str()の結果は必ずチェックし、unwrap_or_default()に依存しないようにしています。- エラーはすべて
ErrorBadRequest(400 Bad Request)で統一し、実運用時に panic が起きるリスクを排除しました。
|
1 2 3 4 5 |
#[get("/secure")] async fn secure_endpoint(key: ApiKey) -> impl Responder { format!("API Key received: {}", key.0) } |
リクエスト抽出器(Path, Query, Json)の実践的使用法
Path, Query, Json は型安全にリクエストデータを取得できる標準抽出器です。ここでは「複数パラメータの受け取り」「バリデーションとエラーハンドリング」の具体例を示します。
Path と Query の型安全な取得例
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
use actix_web::{get, web, HttpResponse, Responder}; /// GET /users/{id}?verbose=true にマッピングされるハンドラ #[get("/users/{id}")] async fn get_user( path: web::Path<(u32,)>, // タプルで複数パラメータも取得可 query: web::Query<UserOpts>, ) -> impl Responder { let user_id = path.into_inner().0; if query.verbose { HttpResponse::Ok() .body(format!("User {} (verbose)", user_id)) } else { HttpResponse::Ok() .body(format!("User {}", user_id)) } } /// /users エンドポイントのクエリパラメータ #[derive(serde::Deserialize)] struct UserOpts { verbose: bool, } |
Path<(u32,)>のようにタプルで受け取ると、複数のパス変数をまとめて取得できます。Query<T>は URL エンコードされたクエリ文字列を自動的にデシリアライズし、型チェックが走ります。
Json ペイロードのバリデーションとエラーハンドリング
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
use actix_web::{post, web, HttpResponse, Responder}; use validator::Validate; /// POST /users に対する JSON ボディ受信ハンドラ #[post("/users")] async fn create_user(payload: web::Json<NewUser>) -> impl Responder { // バリデーションエラーは 400 Bad Request に変換 if let Err(e) = payload.validate() { return HttpResponse::BadRequest().json(e); } // 実装例では DB 保存は別ハンドラで行う(省略) HttpResponse::Created() .body(format!("User {} created", payload.name)) } /// JSON で受け取る構造体 #[derive(serde::Deserialize, Validate)] struct NewUser { #[validate(length(min = 1))] name: String, #[validate(email)] email: String, } |
validatorクレートを併用すると、フィールド単位の検証ロジックが簡潔に書けます。- バリデーション失敗時はエラーメッセージを JSON で返すことでクライアント側の実装が楽になります。
非同期データベース呼び出しと統一的エラーハンドリング
Actix Web のハンドラは async が前提なので、非同期対応 ORM/ドライバ(例: sqlx, sea‑orm)をそのまま await できます。さらに、エラー型をカスタマイズし ResponseError を実装すれば、HTTP ステータスコードとメッセージを一元管理できます。
sqlx を用いた async DB クエリの実装パターン
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
use actix_web::{get, web, HttpResponse, Responder}; use sqlx::postgres::PgPool; use uuid::Uuid; /// GET /items/{id} でアイテムを取得 #[get("/items/{id}")] async fn get_item( pool: web::Data<PgPool>, path: web::Path<Uuid>, ) -> Result<impl Responder, ApiError> { let id = path.into_inner(); // sqlx のクエリは .fetch_one().await で結果取得 let row = sqlx::query_as!( Item, "SELECT id, name, price FROM items WHERE id = $1", id ) .fetch_one(pool.get_ref()) .await .map_err(ApiError::Database)?; Ok(HttpResponse::Ok().json(row)) } /// DB から取得したレコードを表す構造体 #[derive(serde::Serialize)] struct Item { id: Uuid, name: String, price: i32, } |
web::Data<PgPool>はアプリ全体で共有できるコネクションプールです。query_as!マクロはコンパイル時に SQL の型チェックを行うため、実行時エラーが減ります。
ResponseError を実装したカスタムエラーで Result を Propagate する方法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
use actix_web::{http::StatusCode, HttpResponse, ResponseError}; use derive_more::Display; use std::fmt; /// ハンドラ全体で使用するエラー型 #[derive(Debug, Display)] enum ApiError { #[display(fmt = "Database error: {}", _0)] Database(sqlx::Error), #[display(fmt = "Item not found")] NotFound, } impl ResponseError for ApiError { fn status_code(&self) -> StatusCode { match *self { ApiError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, ApiError::NotFound => StatusCode::NOT_FOUND, } } fn error_response(&self) -> HttpResponse { // エラーメッセージは JSON で返す let body = serde_json::json!({ "error": self.to_string() }); HttpResponse::build(self.status_code()).json(body) } } |
Result<impl Responder, ApiError>と書くことで、?演算子でエラーを自動的にハンドラ外へ伝搬できます。ResponseError実装により、ステータスコードとレスポンスボディが一元管理でき、テスト時の期待値設定も楽になります。
ブロッキング処理の安全なオフロード
CPU バウンドや同期 I/O(例: 既存の Diesel 同期クエリ)を非同期ハンドラから直接呼び出すと、Actix のスレッドプールがブロックされて全体性能が低下します。web::block を使えば、安全に別スレッドへオフロードできます。
web::block を利用した CPU バウンド処理の切り離し
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
use actix_web::{post, web, HttpResponse, Responder}; use std::time::Instant; /// 重い計算(例: 画像ハッシュ生成)をオフロード #[post("/hash")] async fn compute_hash(payload: web::Bytes) -> impl Responder { // web::block は Result<T, E> を返す Future を受け取る let result = web::block(move || heavy_hash(&payload)).await; match result { Ok(hash) => HttpResponse::Ok().body(hash), Err(_) => HttpResponse::InternalServerError() .body("Hash computation failed"), } } /// 同期関数:CPU バウンド処理の例 fn heavy_hash(data: &[u8]) -> Result<String, std::io::Error> { // 時間がかかる計算をシミュレート let start = Instant::now(); let hash = format!("{:x}", md5::compute(data)); println!("hash computed in {:?}", start.elapsed()); Ok(hash) } |
web::blockは内部でブロッキング用スレッドプール(デフォルトはactix_rt::Systemのブロッキングスレッド)を使用し、呼び出し元の非同期タスクは即座に復帰します。- エラーは
await時点でResultにラップされるので、ハンドラ側では普通にmatchすれば OKです。
ハンドラの単体テストとプロジェクト設定
実務ではコード変更時にハンドラ単体テストを走らせることで不具合を早期発見できます。Actix はテスト用ユーティリティ actix_web::test を提供しており、モックリクエストの作成やレスポンス検証が簡潔に書けます。また、プロジェクト全体の Cargo 設定例も合わせて示します。
actix_web::test でモックリクエストを作成しハンドラを検証
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#[cfg(test)] mod tests { use super::*; use actix_web::{http::StatusCode, test, web, App}; #[actix_rt::test] async fn test_get_user_verbose() { // ハンドラだけを組み込んだ最小構成のアプリケーション let app = test::init_service( App::new().service(get_user) // get_user は前述のハンドラ ) .await; // /users/42?verbose=true のリクエストを作成 let req = test::TestRequest::get() .uri("/users/42?verbose=true") .to_request(); // ハンドラにリクエストを投げてレスポンス取得 let resp = test::call_service(&app, req).await; assert_eq!(resp.status(), StatusCode::OK); // ボディ文字列を検証(Bytes → &str へ変換) let body_bytes = test::read_body(resp).await; let body_str = std::str::from_utf8(&body_bytes).unwrap(); assert_eq!(body_str, "User 42 (verbose)"); } #[actix_rt::test] async fn test_create_user_validation_error() { let app = test::init_service( App::new().service(create_user) // create_user は前述のハンドラ ) .await; // 不正な JSON(email が欠落) let payload = r#"{"name":"Alice"}"#; let json_value: serde_json::Value = serde_json::from_str(payload).unwrap(); let req = test::TestRequest::post() .uri("/users") .set_json(&json_value) .to_request(); let resp = test::call_service(&app, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } |
test::init_serviceでハンドラだけを組み込んだ最小構成のアプリケーションを作ります。TestRequestによりメソッド・URI・ボディなど自由に設定でき、実際のクライアントリクエストと同等です。read_bodyが返すbytes::Bytesは UTF‑8 と仮定してデコードするか、直接 JSON デシリアライズして検証できます。
Cargo.toml の推奨設定(TLS 実装は rustls に統一)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
[package] name = "actix_async_example" version = "0.1.0" edition = "2021" [dependencies] # TLS には OpenSSL 依存のない rustls を使用することでビルドが軽くなる actix-web = { version = "4.5", features = ["rustls"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" validator = { version = "0.16", features = ["derive"] } # 非同期 PostgreSQL 用ドライバ。runtime は tokio、TLS は rustls を選択 sqlx = { version = "0.7", features = [ "postgres", "runtime-tokio-rustls", "macros" ] } tokio = { version = "1.38", features = ["full"] } # テスト用に actix-rt が必要(actix-web の dev-dependencies でも可) [dev-dependencies] actix-rt = "2.9" |
設定のポイント
| 項目 | 推奨設定 | 理由 |
|---|---|---|
edition |
"2021" |
最新コンパイラ機能と安全な非同期コードが利用可能 |
| Actix の TLS 実装 | features = ["rustls"] |
OpenSSL に依存せず、クロスプラットフォームで軽量 |
sqlx の runtime と TLS |
"runtime-tokio-rustls" |
Tokio と rustls が統一され、ビルドがシンプル |
| バリデーション | validator の derive 機能 |
構造体に直接バリデーションロジックを付与できる |
この設定例は Actix Web v4 と Rust 2021 エディションの組み合わせで、CI 環境やローカル開発でも問題なくビルドできます。
まとめ
- シンプルなハンドラ は
async fn -> impl Responderで実装し、テストが容易です。 - カスタム抽出器 を作るときは必ずヘッダー文字列の変換エラーをチェックし、
unwrapに頼らない実装にしましょう。 - データベースアクセス は
Result<_, ApiError>として統一的にエラーハンドリングし、ResponseErrorで HTTP ステータスと JSON メッセージを一本化します。 - ブロッキング処理 は
web::blockで安全にオフロードし、サーバ全体のスループット低下を防ぎます。 - 単体テスト は
actix_web::testを活用してハンドラ単位で検証し、CI パイプラインに組み込むことで品質を保ちます。
以上のベストプラクティスを取り入れれば、Actix Web アプリケーションは安全・高速・保守性の高いコードベースになるでしょう。