axum crate の Router (3) Router::fallback
前回は axum crate の Router がどう動くのかをある程度追いかけてみました 。
今回は axum crate の Router::fallback メソッドの動きを追いかけてみようと思います。
今回も axum crate のバージョンは 0.8.6 です。
前回のおさらい
前回の内容から一部おさらいします。前回の記事で確認した Router::call_with_state を改めて確認します。
https://github.com/tokio-rs/axum/blob/axum-v0.8.6/axum/src/routing/mod.rs#L417-L432
pub(crate) fn call_with_state(&self, req: Request, state: S) -> RouteFuture<Infallible> {
let (req, state) = match self.inner.path_router.call_with_state(req, state) {
Ok(future) => return future,
Err((req, state)) => (req, state),
};
let (req, state) = match self.inner.fallback_router.call_with_state(req, state) {
Ok(future) => return future,
Err((req, state)) => (req, state),
};
self.inner
.catch_all_fallback
.clone()
.call_with_state(req, state)
}
path_router → fallback_router → catch_all_fallback と順に call_with_state を試します。
PathRouter::call_with_state はマッチするルートがなければ失敗します。
path_router でマッチするルートがなければ fallback_router に来る、というわけです。
これを踏まえて fallback を見ていきます。
Router::fallback
Router::fallback のドキュメントには次のようなことが書かれています。
- fallback handler をルーターに追加するメソッド
- ルートにマッチしなかったときに呼び出される
- ハンドラーが 404 を返しても呼び出されない
確認してきた実装の説明が簡潔に書いてあります。 3 点目の注意事項も大切です。ハンドラーが HTTP 404 (NotFound) などのエラーを返しても fallback handler は呼び出されません。
Router::method_not_allowed_fallback
コード例の前にもうひとつメソッドに触れたいです。
Router::method_not_allowed_fallback です。
Router::fallback はパスにマッチしなかったときに使用されますが、パスにマッチかつメソッドにマッチしなかったときには使用されません。
既定では 405 Method Not Allowed が返されます。ここを変更する場合は Router::method_not_allowed_fallback にハンドラーを指定できます。
注意事項としては、メソッドにマッチしなかかったとしても、そもパスにマッチしなかったときは Router::fallback のハンドラーが使用される点です。ややこしいですね……。
実装として前回見たとおり PathRouter → MethodRouter で処理しているからですね。
Router::method_not_allowed_fallback の実装を見ると、 self.inner.fallback_router ではなく self.inner.path_router に設定されています。
https://docs.rs/axum/0.8.6/src/axum/routing/mod.rs.html#374-383
pub fn method_not_allowed_fallback<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
tap_inner!(self, mut this => {
this.path_router
.method_not_allowed_fallback(handler.clone());
})
}
さらに PathRouter::method_not_allowed_fallback では MethodRouter に default_fallback で設定しています。
https://github.com/tokio-rs/axum/blob/axum-v0.8.6/axum/src/routing/path_router.rs#L116-L126
pub(super) fn method_not_allowed_fallback<H, T>(&mut self, handler: H)
where
H: Handler<T, S>,
T: 'static,
{
for (_, endpoint) in self.routes.iter_mut() {
if let Endpoint::MethodRouter(rt) = endpoint {
*rt = rt.clone().default_fallback(handler.clone());
}
}
}
コード例
さて、遠回りしたのですが、コード例です。試しに 404 と 405 を 200 にして body に fallback だと分かる文字列を含めます。
async fn fallback_handler() -> impl axum::response::IntoResponse {
(axum::http::StatusCode::OK, "fallback")
}
async fn method_not_allowed_fallback() -> impl axum::response::IntoResponse {
(axum::http::StatusCode::OK, "method_not_allowed_fallback")
}
fn router() -> axum::Router<()> {
axum::Router::new()
.route("/", axum::routing::get(root))
.route("/users", axum::routing::post(create_user))
.route("/users/{user_id}", axum::routing::get(get_user))
.fallback(fallback_handler)
.method_not_allowed_fallback(method_not_allowed_fallback)
}
// テストコード
#[tokio::test]
async fn test_path_not_found() -> anyhow::Result<()> {
let router = router();
let request = axum::http::Request::builder()
.method(axum::http::Method::GET)
.uri("/unknown")
.body(axum::body::Body::empty())?;
let response = send_request(router, request).await?;
// no fallback => HTTP 404
// assert_eq!(response.status(), axum::http::StatusCode::NOT_FOUND);
// assert_eq!(response.into_body_string().await?, "");
assert_eq!(response.status(), axum::http::StatusCode::OK);
assert_eq!(response.into_body_string().await?, "fallback");
Ok(())
}
#[tokio::test]
async fn test_method_not_allowed() -> anyhow::Result<()> {
let router = router();
let request = axum::http::Request::builder()
.method(axum::http::Method::POST)
.uri("/")
.body(axum::body::Body::empty())?;
let response = send_request(router, request).await?;
// no fallback => HTTP 405
// assert_eq!(
// response.status(),
// axum::http::StatusCode::METHOD_NOT_ALLOWED
// );
// assert_eq!(response.into_body_string().await?, "");
assert_eq!(response.status(), axum::http::StatusCode::OK);
assert_eq!(
response.into_body_string().await?,
"method_not_allowed_fallback"
);
Ok(())
}
#[tokio::test]
async fn test_other_default_fallback() -> anyhow::Result<()> {
let router = router();
let request = axum::http::Request::builder()
.method(axum::http::Method::POST)
.uri("/unknown")
.body(axum::body::Body::empty())?;
let response = send_request(router, request).await?;
// no fallback => HTTP 404
// assert_eq!(response.status(), axum::http::StatusCode::NOT_FOUND);
// assert_eq!(response.into_body_string().await?, "");
assert_eq!(response.status(), axum::http::StatusCode::OK);
assert_eq!(response.into_body_string().await?, "fallback");
Ok(())
}
テストコードは次回詳細に触れますが、ドキュメントどおり・期待どおりの動きになっていることが分かります。
おわりに
今回は axum crate の Router::fallback (と Router::method_not_allowed_fallback) について書きました。 fallback まわりの動作については merge や nest のタイミングで再度触れるかもしれません。
次回は axum crate のテストについて書きたいと思います。
Discussion