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

2023/07/23

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())
}
Rust🦀, Network⚡, PostgreSQL🐘, Unity🎮

Tags

rust  (9)
rocket  (7)
svelte  (5)
c++  (4)
vscode  (3)
sqlx  (3)
glfw  (2)
opengl  (2)
nestjs  (2)
render  (2)
wsl2  (2)
goerli  (1)
geth  (1)
nft  (1)
gui  (1)
tetris  (1)
jwt  (1)
prisma  (1)
urql  (1)
mdsvex  (1)
tmux  (1)
nvim  (1)
axum  (1)
vim  (1)
pacman  (1)
Cursor  (1)
VSCode  (1)
PHP  (1)