はじめに
今現在書いている「cloudflareを利用してwebアプリを公開したい!」でaxumを使用しています。
第一回記事はこちらになります。
これより先の段階へと進む前に、axumのエラーハンドリングについてちゃんと学んでおこうと思います。
基本的にはRustDocsの内容を簡単に和訳するだけです。誤訳もあるかと思われますので、その場合はコメントで指摘いただけますと幸いです。
要点
- axumはIntoResponseトレイトを実装しているものをResponseにして返せる
- anyhowとIntoResponseを使って任意のエラーをResponseに変換することができる
- Errの型にIntoResponseトレイトが実装されていればエラーが出たとき?で返却することができる
- HandleErrorを使ってサービス内で発生したエラーを扱うことができる
- HandleErrorLayerを使ってそのレイヤー内でのエラーハンドリングを設定できる
基本
axumはすべてのサービスにエラー型としてInfallible型を要求します。
この場合、以下のコードはResultの中身がErrになってもResponse型に置き換えてクライアントに返送します。
use axum::http::StatusCode;
async fn handler() -> Result<String, StatusCode> {
// ...
}
これはErrの時の型にIntoResponseトレイトが実装されているからです。
ステータスコードの他にも色々な値をレスポンスで返せて、なおかつ?を使用しても問題なく返却できます。
IntoResponseトレイトの利用(anyhowクレート利用)
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Router,
};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("listening on {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
async fn handler() -> Result<(), AppError> {
try_thing()?;
Ok(())
}
fn try_thing() -> Result<(), anyhow::Error> {
anyhow::bail!("it failed!")
}
// Make our own error that wraps `anyhow::Error`.
struct AppError(anyhow::Error);
// Tell axum how to convert `AppError` into a response.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
}
}
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
// `Result<_, AppError>`. That way you don't need to do that manually.
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}
この例ではanyhowを使って、AppError型を作成しています。
AppError型には、IntoResponseトレイトとFromトレイトが実装されています。
AppError型は、自身にステータスコードとエラーメッセージを持てるようになっています。
FromトレイトでAnyhowのError型をAppErrorのErrに束縛します。
束縛されたErrはIntoResponseトレイトでステータスコード"500"とエラーメッセージ"Something went wrong: {渡されたエラーメッセージ}"を持つレスポンスに変換されます。
この例の処理の流れは、"/"へのリクエストを受けると、handler->try_thingが呼び出され、try_thingがanyhowのエラーを返します。
ここでエラーが発生するので?によってリターンされます。この時、try_thing()が返してくるのはErr(anyhow::Error)なので、AppErrorに変換され、ステータスコード"500"、エラーメッセージ"Something went wrong: it failed!"となるはずです。
RustDocsにはanyhowなしでIntoResponseトレイトを実装する例が載っていますが、冗長になるので基本的にはanyhowを使うのがよさそうです。
HandleErrorの利用
HandleErrorを使うと、その処理の中でエラーが生成されたときにどのようなレスポンスを返すかを決めることができます。
use axum::{
Router,
body::Body,
http::{Request, Response, StatusCode},
error_handling::HandleError,
};
async fn thing_that_might_fail() -> Result<(), anyhow::Error> {
// ...
}
// this service might fail with `anyhow::Error`
let some_fallible_service = tower::service_fn(|_req| async {
thing_that_might_fail().await?;
Ok::<_, anyhow::Error>(Response::new(Body::empty()))
});
let app = Router::new().route_service(
"/",
// we cannot route to `some_fallible_service` directly since it might fail.
// we have to use `handle_error` which converts its errors into responses
// and changes its error type from `anyhow::Error` to `Infallible`.
HandleError::new(some_fallible_service, handle_anyhow_error),
);
// handle errors by converting them into something that implements
// `IntoResponse`
async fn handle_anyhow_error(err: anyhow::Error) -> (StatusCode, String) {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {err}"),
)
}
HandleError::new([呼び出す処理],[エラーのハンドラ])で記述します。
この例では、some_failable_serviceを呼び出し、その中でthing_that_might_fallを呼び出しています
thing_that_might_fallがOkだった場合はResponse::new(Body::empty())を返します。
Errとなった場合は、HandleErrorによりhandle_anyhow_errorでレスポンスに変換します。
HandleErrorLayerの利用
HandleErrorLayerを使うことで、ミドルウェアで発生したエラーを扱うことができるようになります。
use axum::{
Router,
BoxError,
routing::get,
http::StatusCode,
error_handling::HandleErrorLayer,
};
use std::time::Duration;
use tower::ServiceBuilder;
let app = Router::new()
.route("/", get(|| async {}))
.layer(
ServiceBuilder::new()
// `timeout` will produce an error if the handler takes
// too long so we must handle those
.layer(HandleErrorLayer::new(handle_timeout_error))
.timeout(Duration::from_secs(30))
);
async fn handle_timeout_error(err: BoxError) -> (StatusCode, String) {
if err.is::<tower::timeout::error::Elapsed>() {
(
StatusCode::REQUEST_TIMEOUT,
"Request took too long".to_string(),
)
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {err}"),
)
}
}
この例では、HandleErrorLayerをつけることで、このレイヤーより内側の処理でエラーが発生または30秒以上経過した場合はhandle_timeout_errorが呼び出されます。
HandleErrorLayerをExtractorに適用する
HandleErrorLayerにはハンドラに引数としてExtractorで抽出できる値を渡せます。
use axum::{
Router,
BoxError,
routing::get,
http::{StatusCode, Method, Uri},
error_handling::HandleErrorLayer,
};
use std::time::Duration;
use tower::ServiceBuilder;
let app = Router::new()
.route("/", get(|| async {}))
.layer(
ServiceBuilder::new()
// `timeout` will produce an error if the handler takes
// too long so we must handle those
.layer(HandleErrorLayer::new(handle_timeout_error))
.timeout(Duration::from_secs(30))
);
async fn handle_timeout_error(
// `Method` and `Uri` are extractors so they can be used here
method: Method,
uri: Uri,
// the last argument must be the error itself
err: BoxError,
) -> (StatusCode, String) {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("`{method} {uri}` failed with {err}"),
)
}
この例では、Extractorからmethodとuriを、エラーの発生元からエラー内容を引数に受け取りエラーハンドリングをします。
おわりに
今回はaxum::error_handlingのRustDocsを読み、axumにおけるエラーハンドリングの手法について勉強してみました。IntoResponseトレイトの実装を覚えて、色々な方法でエラーハンドリングを実装できるようにしてみましょう。