Rustのwebフレームワーク、actix-webのエラーを拡張して便利に使おう

## 概要

Rustのwebフレームワーク、actix-webには各種HTTPエラーレスポンスを返す機能があります。そうしたエラーにより詳細な情報を付加したいとき、actix-webのエラートレイトを実装したenumを使うと便利です。この記事ではその方法をご紹介します。

## 環境

```bash
$ rustc --version
rustc 1.45.0 (5c1f21c3b 2020-07-13)

$ cargo --version
cargo 1.45.0 (744bd1fbb 2020-06-15)
```

## actix-web

actix-webはRust用のwebフレームワークです。単体でHTTPサーバとして機能し、静的ファイルのやり取りやweb APIを実装することができます。

- [公式ドキュメント](https://actix.rs/)
- [\[Rust\] actix-webを使ってみよう](https://dev.classmethod.jp/articles/actix-web/)

動作がやたら早いことが推しポイントらしく、ドキュメントにベンチマークサイトへのリンクが乗っていたりします。

- [Round 19 results - TechEmpower Framework Benchmarks](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=fortune)

(2019年のRound 18では1位でしたが2020年のRound 19では惜しくも2位に。がんばれ！)

## actix-webでのAPI例

まずactix-webを使った簡単なAPIを実装してみましょう。

```bash
$ cargo new api_example --bin
```

Cargo.tomlの依存関係には以下のように記載します。

```toml
[dependencies]
# actix-webのクレート群
actix-web = "2.0"
actix-rt = "1.0"
# リクエスト/レスポンスのJSONをシリアライズ/デシリアライズする用
serde = { version = "1.0", features = ["derive"] }
```

main.rsは以下のように書きましょう。

```rust
use actix_web::{web, App, HttpResponse, HttpServer};
use serde::{Deserialize, Serialize};

#[derive(Deserialize)]
pub struct RequestBody {
    id: u16,
}

#[derive(Serialize)]
pub struct ResponseBody {
    message:String
}

// APIハンドラ
// 以下の例ではここを書き換えてレスポンスを変化させていきます
async fn index(request_body: web::Json<RequestBody>) -> actix_web::Result<HttpResponse> {
    let message = format!("私のIDは{}です", request_body.id);
    Ok(HttpResponse::Ok().json(ResponseBody {
        message
    }))
}

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
        // ここでハンドラとエンドポイントを対応付け
            .route("/api", web::post().to(index))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}
```

actix-webのハンドラ関数は`actix_web::Result`をreturnすると、自動でHTTPレスポンスを構成してくれます。

この例ではJSONのリクエストを受け取って、JSONのレスポンスを返します。例えば`curl`コマンドで以下のように叩いてみましょう。

```bash
$ curl -H "Content-Type: application/json" -d '{"id":1}' localhost:8080/api
```

以下のようなレスポンスがかえってきます。

```bash
< HTTP/1.1 200 OK
< content-length: 32
< content-type: application/json
< date: Tue, 15 Sep 2020 03:57:19 GMT
<
{"message":"私のIDは1です"}
```

## エラーの実装(基本編)

本題に戻ってエラーを返していきましょう。簡単な例を出すと、以下のようにして`401 Unauthorized`のレスポンスを返すことができます。

```rust
async fn index(_: web::Json<RequestBody>) -> actix_web::Result<HttpResponse> {
    return Ok(HttpResponse::Unauthorized().finish());
}
```

さらに、レスポンスにJSON形式のbodyを追加することもできます。

```rust
async fn index(_: web::Json<RequestBody>) -> actix_web::Result<HttpResponse> {
    return Ok(HttpResponse::Unauthorized().json(ResponseBody {
        message: String::from("need authentification"),
    }));
}
```

```bash
< HTTP/1.1 401 Unauthorized
< content-length: 35
< content-type: application/json
< date: Mon, 14 Sep 2020 13:09:03 GMT
<
{"message":"need authentification"}
```

シンプルなエラーを返すだけならこれで事足りますが、例えば同じ`401 Unauthorized`でも「認証されていない場合」/「認証に失敗した場合」などを分けたい場合が出てきます。
こうしたときに、actix-webのエラートレイトを実装したenumを使うと便利になります。

## エラーの実装(応用編)

まずはエラーをenumとして定義します。

```rust
use std::fmt::{Debug};
use serde::{Serialize};

#[derive(Debug, Serialize)]
pub enum ApiErrorResponse {
    AuthenticationFailure,
    NeedsAuthentication,
}
```

次にエラーレスポンスの中身の構造体も定義しておきましょう。

```rust
#[derive(Debug, Serialize)]
pub struct ApiResponseBody {
    pub success: bool,
    pub errcode: u16,
    pub message: String,
}
```

そして先ほど定義したenum、`ApiErrorResponse`に`actix_web::error::ResponseError`トレイトを実装します。
そうすることで、`actix_web::Result<HttpResponse, ApiErrorResponse>`をハンドラ関数から返すことができるようになります。

`actix_web::error::ResponseError`トレイトを実装するために`status_code()`と`error_response()`メソッドを実装する必要があります。

`status_code()`は`actix_web::http::StatusCode`enumのいずれかを返す関数です。今回はいずれも`401 Unauthorized`を返したいので`StatusCode::UNAUTHORIZED`を返すよう実装します。
エラー構造体をenumで定義しておくことでmatch文で記述できて便利です。

`error_response()`は`actix_web::HttpResponse`を返す関数です。ここもmatch文を使って先ほど定義した`status_code()`とJSON形式のレスポンスボディを追加して`HttpResponse`を構築しています。

```rust
use actix_web::{HttpResponse, http::StatusCode};

impl actix_web::error::ResponseError for ApiErrorResponse {
    fn status_code(&self) -> StatusCode {
        match *self {
            ApiErrorResponse::AuthenticationFailure => StatusCode::UNAUTHORIZED,
            ApiErrorResponse::NeedsAuthentication => StatusCode::UNAUTHORIZED,
        }
    }
    fn error_response(&self) -> HttpResponse {
        match *self {
            ApiErrorResponse::AuthenticationFailure => HttpResponse::build(self.status_code())
                .json(ApiResponseBody {
                    success: false,
                    errcode: 100,
                    message: String::from("authentication failure"),
                }),
            ApiErrorResponse::NeedsAuthentication => {
                HttpResponse::build(self.status_code()).json(ApiResponseBody {
                    success: false,
                    errcode: 110,
                    message: String::from("need authentication"),
                })
            }
        }
    }
}
```

`actix_web::error::ResponseError`トレイトを実装するために`Debug`と`Display`を実装する必要もあるので適宜実装しておきます。(Debugはマクロで実装済み)

```rust
use std::fmt::{Debug, Display, Formatter};
use serde_json::to_string_pretty;

impl Display for ApiErrorResponse {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        write!(f, "{}", to_string_pretty(self).unwrap())
    }
}
```

`serde_json::to_string_pretty()`を使うため、Cargo.tomlに以下の依存関係を追記しておきます。

```toml
[dependencies]
serde_json = "1.0"
```

さて、以上のように定義したエラーは以下のように使うことができます。ハンドラの戻り値の型を`actix_web::Result<HttpResponse, ApiErrorResponse>`とすることで、`ApiErrorResponse`をreturnすると定義したHTTPステータスコードとレスポンスボディから構成されたHTTPレスポンスを返してくれます。

enumで定義したことで、戻り値の型は共通で`ApiErrorResponse`を使用することができます。そしてレスポンスボディも毎回記述する必要がなく、ハンドラがすっきりしました。

```rust
async fn index_auth(_: web::Json<RequestBody>) -> actix_web::Result<HttpResponse, ApiErrorResponse> {
    return Err(ApiErrorResponse::AuthenticationFailure);
}

async fn index_other(_: web::Json<RequestBody>) -> actix_web::Result<HttpResponse, ApiErrorResponse> {
    return Err(ApiErrorResponse::NeedsAuthentication);
}

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/api/auth", web::post().to(index_auth))
            .route("/api/other", web::post().to(index_other))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}
```

```bash
$ curl -v -H "Content-Type: application/json" -d '{"id":1}' localhost:8080/api/auth
...
< HTTP/1.1 401 Unauthorized
< content-length: 66
< content-type: application/json
< date: Tue, 15 Sep 2020 03:37:48 GMT
<
{"success":false,"errcode":100,"message":"authentication failure"}


$ curl -v -H "Content-Type: application/json" -d '{"id":1}' localhost:8080/api/other
...
< HTTP/1.1 401 Unauthorized
< content-length: 63
< content-type: application/json
< date: Tue, 15 Sep 2020 03:38:48 GMT
<
{"success":false,"errcode":110,"message":"need authentication"}
```

今回実装したAPIの全体を以下に載せておきます。

```rust
use actix_web::{web, App, HttpResponse, HttpServer, http::StatusCode};
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display, Formatter};
use serde_json::to_string_pretty;

#[derive(Debug, Serialize)]
pub struct ApiResponseBody {
    pub success: bool,
    pub errcode: u16,
    pub message: String,
}

#[derive(Debug, Serialize)]
pub enum ApiErrorResponse {
    AuthenticationFailure,
    NeedsAuthentication,
}

impl Display for ApiErrorResponse {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        write!(f, "{}", to_string_pretty(self).unwrap())
    }
}

impl actix_web::error::ResponseError for ApiErrorResponse {
    fn status_code(&self) -> StatusCode {
        match *self {
            ApiErrorResponse::AuthenticationFailure => StatusCode::UNAUTHORIZED,
            ApiErrorResponse::NeedsAuthentication => StatusCode::UNAUTHORIZED,
        }
    }
    fn error_response(&self) -> HttpResponse {
        match *self {
            ApiErrorResponse::AuthenticationFailure => HttpResponse::build(self.status_code())
                .json(ApiResponseBody {
                    success: false,
                    errcode: 100,
                    message: String::from("authentication failure"),
                }),
            ApiErrorResponse::NeedsAuthentication => {
                HttpResponse::build(self.status_code()).json(ApiResponseBody {
                    success: false,
                    errcode: 110,
                    message: String::from("need authentication"),
                })
            }
        }
    }
}

async fn index_auth(_: web::Json<RequestBody>) -> actix_web::Result<HttpResponse, ApiErrorResponse> {
    return Err(ApiErrorResponse::AuthenticationFailure);
}

async fn index_other(_: web::Json<RequestBody>) -> actix_web::Result<HttpResponse, ApiErrorResponse> {
    return Err(ApiErrorResponse::NeedsAuthentication);
}

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/api/auth", web::post().to(index_auth))
            .route("/api/other", web::post().to(index_other))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}
```

## まとめ

- Rustのactix-webは高速なwebフレームワーク
- actix-webはハンドラから`actix_web::Result`型を返すと自動でHTTPレスポンスを構成する
- より詳細な情報を持ったエラーを返したい場合はenumを定義して`actix_web::error::ResponseError`トレイトを実装する

## 参考文献

- [Errors(actix公式ドキュメント)](https://actix.rs/docs/errors/)

エラーを実装する際のポイントやベストプラクティスが掲載されています。本記事での例もそれに従って実装しました。

- [actix_web - Rust](https://docs.rs/actix-web/3.0.1/actix_web/index.html)

クレートのドキュメント。詳しくはこちらを参照。