gRPCサーバーのエラーハンドリング(tonic, thiserror)
RustのtonicでgRPCサーバーを書くときのエラーハンドリングについて
モチベーション
エラーはエラーコードで扱いたい
thiserrorを使いたい
各エンドポイントの実装(サービス)では、いちいちmap_errせずに、?で直接変換したい
ボイラープレートをなくして、サービスの実装の可読性を上げたい
課題
ナイーブな実装だと別のcrateのエラーを?でStatusに変換できない
例えば、サーバーとprotobufのcrateを分けている場合で、protobuf側のエラーproto::ErrorをStatusに?で変換したい
このときの実装手段として
直接、map_errやmatchなどで変換する
-) ボイラープレート
proto側のcrateに、From<Error> for Statusを実装
-) protoはtonicに依存したくないし、なにのエラーを返すかはあくまでサーバーの責務で決めたい
ちなみに、サーバー側のcrateでFrom<proto::Error> for Statusはコンパイラ制約でできない
proto::ErrorをサーバーのcrateのthiserrorのError型に変換して、それをStatusに変換
+) 期待を満たす
冗長だが、以下のように、tonicのtraitのメソッドの中に直接サービスを実装せず、別のstructに実装してwrapしている。
なぜなら、一度crate::Errorを経由してStatusに変換するため
もし直接変換しようとすると、From<proto::Error> for Statusが実装されてないとコンパイラに叱られる
code::rs
impl PlaylistService for PlaylistServiceController {
async fn get_playlist(
&self,
request: Request<pb::GetPlaylistRequest>,
) -> Result<Response<pb::Playlist>, Status> {
Ok(Response::new(
self.service.get_playlist(request.into_inner()).await?,
))
}
}
impl PlaylistServiceImpl {
async fn get_playlist(&self, request: pb::GetPlaylistRequest) -> Result<pb::Playlist, Error> {
let id = pb::Playlist::extract_id(&request.name)?;
let playlist = self.repo.get(id).await?;
Ok(playlist.into())
}
}
thiserrorで、サーバー内のエラー型を定義
サーバー内のエラーを、このError型で一元的に管理し、gRPCのエラーに変換しているので、
デバッグが楽
謎のエラーがユーザー側に漏れ出ることがない
ユーザー向けのエラー実装(メッセージなど)を一箇所にまとめられる
code::rs
use tonic::Status;
pub enum Error {
InvalidArgument,
NotFound,
AlreadyExists,
Internal,
ProtoError(#from proto::errors::Error), }
/// Error representation for gRPC server
impl From<Error> for Status {
fn from(err: Error) -> Self {
match err {
Error::InvalidArgument | Error::ProtoError(proto::errors::Error::InvalidArgument) => {
Status::invalid_argument(err.to_string())
}
Error::NotFound => Status::not_found(err.to_string()),
Error::AlreadyExists => Status::already_exists(err.to_string()),
Error::Internal => Status::internal(err.to_string()),
}
}
}
// NOTE: cannot convert pb::Error to Status directly. So convert pb::Error -> crate::Error -> Status,
// errorE0117: only traits defined in the current crate can be implemented for types defined outside of the crate // impl From<proto::errors::Error> for Status {
// fn from(err: proto::errors::Error) -> Self {
// match err {
// Error::InvalidArgument => Status::invalid_argument(err.to_string()),
// }
// }
// }