🍂

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_routerfallback_routercatch_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 のハンドラーが使用される点です。ややこしいですね……。

実装として前回見たとおり PathRouterMethodRouter で処理しているからですね。

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 では MethodRouterdefault_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 だと分かる文字列を含めます。

https://github.com/bouzuya/rust-examples/blob/4251464a7acba78efe4383d47c4d74143e9649c4/axum5/src/main.rs

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 まわりの動作については mergenest のタイミングで再度触れるかもしれません。

次回は axum crate のテストについて書きたいと思います。

GitHubで編集を提案
ドクターメイト

Discussion