Logicky Blog

Logickyの開発ブログです

RustのRocketでエラーハンドリング

Rocketで controller のハンドラ関数でリクエストを受けて、use_case や repository で詳細の処理を実行するようになっています。use_case, repository で発生したエラー毎にレスポンス時のステータスコードを決めないといけないです。 ですので、use_case, repository は基本全てResult型を返し、Errはオリジナルのstatus_codeを持つAppError型を使おうと思っています。use_case, repository 等でエラーが発生しうるハンドラ関数も、基本的にResult型を返します。 Rocket はここに書いていますが、Responderトレイトが実装されている型であれば、何でもハンドラ関数の戻り値に設定できます。ですので、AppError型にResponderトレイトを実装します。

オリジナルの AppError 型

こういうやつを想定しています。

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
pub struct AppError {
    pub status_code: u16,
    pub message: String,
}

impl AppError {
    pub fn new(status_code: u16, message: &str) -> Self {
        Self {
            status_code,
            message: message.to_string(),
        }
    }
}

new関数を作ってますので、Err(AppError::new(404, "Not Found"))みたいな感じでエラーを作れます。

どんなエラーでも簡単に AppError に変換できるようにする

Result型のErrの中身は何でもよいのですが、一般的には、std::error::Errorトレイトが実装されているようです。これは、Displayトレイトも必要になりますので、to_string()が使えます。 Result<T, E>で、Estd::error::Errorトレイトを実装している場合、app_error()関数を使えるようにして、簡単にAppErrorに変換できるようにしてみます。

pub trait AppErrorResultExt<T> {
    fn app_error(self, status_code: u16, message: &str) -> Result<T, AppError>;
}

impl<T, E> AppErrorResultExt<T> for Result<T, E>
where
    E: std::error::Error,
{
    fn app_error(self, status_code: u16, message: &str) -> Result<T, AppError> {
        self.map_err(|_| AppError::new(status_code, message))
    }
}

これで、下記のように使えます。parse()i32型に変換しようとしますが、130aは変換に失敗しますので、Err<ParseIntError>を返します。app_error関数はErrの場合、AppError::newを実行して、Err<AppError>を返します。?によりErrの場合は、自動的に return されますので、結果的にErr<AppError>が返されます。もし"130".parse()など、変換が成功する場合は、app_errorは何もしませんし、?Ok()の囲いが取れて、変換後のi32の値がnumに返されます。

let num:i32 = "130a".parse().app_err(400, "Bad Request")?;

Rocket のレスポンスで使えるようにする

先述のとおり、Rocket のレスポンスに使うには、Responderトレイトを実装する必要があります。ドキュメントには下記のように Deriving を使うのがおすすめと書いてありました。

use rocket::http::ContentType;
use rocket::serde::{Serialize, json::Json};

#[derive(Responder)]
enum Error<T> {
    #[response(status = 400)]
    Unauthorized(Json<T>),
    #[response(status = 404)]
    NotFound(Template, ContentType),
}

今回は、ステータスコードが動的に変わるので、ちょっと上記は使えないかなあと思い、下記のようにしてみました。

use rocket::http::Status;
use rocket::serde::json::Json;
use rocket::response::{Responder, Response};
use rocket::Request;
use serde::{Serialize, Deserialize};

impl<'r> Responder<'r, 'static> for AppError {
    fn respond_to(self, req: &'r Request<'_>) -> rocket::response::Result<'static> {
        let status = Status::from_code(self.status_code).unwrap_or(Status::InternalServerError);
        Response::build_from(Json(self).respond_to(req)?)
            .status(status)
            .ok()
    }
}

これでハンドラ関数で下記のように使えます。

#[get("/hoge")]
async fn hoge() -> Result<String, AppError> {
    hoge_use_case::hoge().await?;
    Ok("ok!".to_string())
}