ハンドラー

概要

ハンドラーはSalvoフレームワークの核心概念であり、リクエスト処理ユニットと簡単に理解できます。主に2つの用途があります:

  1. エンドポイントとしてHandlerを実装したオブジェクトは、ルーティングシステムに配置され、リクエストを最終処理するエンドポイントとして機能します。#[handler]マクロを使用すると、関数を直接エンドポイントとして使用できます。#[endpoint]マクロを使用すると、エンドポイントとして機能するだけでなく、OpenAPIドキュメントを自動生成できます(この部分は後のドキュメントで詳しく説明します)。

  2. ミドルウェアとして:同じHandlerをミドルウェアとして使用し、リクエストが最終エンドポイントに到達する前または後に処理を行うことができます。

Salvoのリクエスト処理フローは「パイプライン」と見なすことができます:リクエストはまず一連のミドルウェア(縦方向の処理)を通過し、次に一致するエンドポイント(横方向の処理)に到達します。ミドルウェアとエンドポイントの両方がHandlerの実装であるため、システム全体に一貫性と柔軟性が保たれています。

Salvoにおけるハンドラーのフローチャート

flowchart TD
    Client[クライアント] -->|リクエスト| Request[HTTPリクエスト]
    Request --> M1

    subgraph "ハンドラー実装"
        M1[ログ記録ミドルウェア] -->|縦方向処理| M2[認証ミドルウェア] -->|縦方向処理| M3[その他業務ミドルウェア]
        M3 --> Router[ルーティングマッチ]

        Router -->|横方向処理| E1["エンドポイント1<br>#[handler]"]
        Router -->|横方向処理| E2["エンドポイント2<br>#[endpoint]"]
        Router -->|横方向処理| E3["エンドポイント3<br>Handlerトレイト実装"]
    end

    E1 --> Response[HTTPレスポンス]
    E2 --> Response
    E3 --> Response
    Response --> Client

    classDef middleware fill:#afd,stroke:#4a4,stroke-width:2px;
    classDef endpoint fill:#bbf,stroke:#55a,stroke-width:2px;

    class M1,M2,M3 middleware;
    class E1,E2,E3 endpoint;

ミドルウェアとオニオンモデル

オニオンモデルの本質は、ctrl.call_next()の前後の位置によって、リクエストとレスポンスの双方向処理フローを実現し、各ミドルウェアが完全なリクエスト-レスポンスサイクルに参加できるようにすることです。

完全なミドルウェアの例

async fn example_middleware(req: &mut Request, depot: &mut Depot,resp: &mut Response, ctrl: &mut FlowCtrl) {
    // 前処理(リクエスト段階)
    // リクエスト進入時に実行するロジックをここに配置

    // チェーン内の次のハンドラーを呼び出す
    ctrl.call_next(req, depot,resp).await;

    // 後処理(レスポンス段階)
    // リクエスト処理完了後に実行するロジックをここに配置
}
flowchart TB
    Client[クライアント] -->|リクエスト| Request[HTTPリクエスト]

    subgraph "オニオンモデル処理フロー"
        direction TB

        subgraph M1["ミドルウェア1"]
            M1A["前処理<br>ログ記録: リクエスト開始"] --> M1B["後処理<br>ログ記録: リクエスト終了"]
        end

        subgraph M2["ミドルウェア2"]
            M2A["前処理<br>リクエスト開始時間記録"] --> M2B["後処理<br>処理時間計算"]
        end

        subgraph M3["ミドルウェア3"]
            M3A["前処理<br>ユーザー権限検証"] --> M3B["後処理<br>"]
        end

        subgraph Core["コア処理ロジック"]
            Handler["エンドポイントハンドラー<br>#[handler]"]
        end

        Request --> M1A
        M1A --> M2A
        M2A --> M3A
        M3A --> Handler
        Handler --> M3B
        M3B --> M2B
        M2B --> M1B
        M1B --> Response
    end

    Response[HTTPレスポンス] --> Client

    classDef middleware1 fill:#f9d,stroke:#333,stroke-width:2px;
    classDef middleware2 fill:#afd,stroke:#333,stroke-width:2px;
    classDef middleware3 fill:#bbf,stroke:#333,stroke-width:2px;
    classDef core fill:#fd7,stroke:#333,stroke-width:2px;

    class M1A,M1B middleware1;
    class M2A,M2B middleware2;
    class M3A,M3B middleware3;
    class Handler core;

ハンドラーとは

ハンドラーは、Requestリクエストを処理する具体的なオブジェクトです。ハンドラー自体はトレイトであり、内部にhandle非同期メソッドを含みます:

#[async_trait]
pub trait Handler: Send + Sync + 'static {
    async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response);
}

処理関数handleのデフォルトシグネチャには4つのパラメータが含まれ、順に&mut Request, &mut Depot. &mut Response, &mut FlowCtrlです。Depotは一時的なストレージであり、今回のリクエストに関連するデータを保存できます。

使用方法によって、ミドルウェア(hoop)として使用することができ、リクエストが正式なリクエスト処理Handlerに到達する前または後に処理を行うことができます。例えば:ログイン認証、データ圧縮など。

ミドルウェアはRouterhoop関数を通じて追加されます。追加されたミドルウェアは現在のRouterとその内部のすべての子孫Routerに影響を与えます。

Handlerはルーティングマッチに参加し最終的に実行されるHandlerとしても使用でき、goalと呼ばれます。

Handlerをミドルウェア(hoop)として使用

Handlerをミドルウェアとして使用する場合、以下の3種類のミドルウェアをサポートするオブジェクトに追加できます:

  • Service:すべてのリクエストがService内のミドルウェアを通過します。

  • Router:ルーティングマッチが成功した場合のみ、リクエストはServiceで定義されたミドルウェアとマッチしたパス上で収集されたすべてのミドルウェアを順次通過します。

  • Catcher:エラーが発生し、カスタムエラーメッセージが書き込まれていない場合、リクエストはCatcher内のミドルウェアを通過します。

  • HandlerHandler自体はミドルウェアのラップ追加をサポートし、前処理または後処理のロジックを実行します。

#[handler]マクロの使用

#[handler]はコードの記述を大幅に簡素化し、コードの柔軟性を向上させます。

関数に追加してHandlerを実装させることができます:

#[handler]
async fn hello() -> &'static str {
    "hello world!"
}

これは以下と同等です:

struct hello;

#[async_trait]
impl Handler for hello {
    async fn handle(&self, _req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) {
        res.render(Text::Plain("hello world!"));
    }
}

#[handler]を使用すると、コードが非常にシンプルになることがわかります:

  • 手動で#[async_trait]を追加する必要がなくなります。
  • 関数内で不要なパラメータは省略され、必要なパラメータも任意の順序で配置できます。
  • WriterまたはScribe抽象化を実装したオブジェクトは、直接関数の戻り値として使用できます。ここでは&'static strScribeを実装しているため、直接関数の戻り値として返すことができます。

#[handler]は関数だけでなく、structimplにも追加でき、structHandlerを実装させることができます。この場合、implコードブロック内のhandle関数はHandler内のhandleの具体的な実装として認識されます:

struct Hello;

#[handler]
impl Hello {
    async fn handle(&self, res: &mut Response) {
        res.render(Text::Plain("hello world!"));
    }
}

エラー処理

SalvoのHandlerResultを返すことができ、Result内のOkErrの型が両方ともWriterトレイトを実装している必要があります。anyhowの使用が比較的広く普及していることを考慮し、anyhow機能を有効にすると、anyhow::ErrorWriterトレイトを実装します。anyhow::ErrorInternalServerErrorにマッピングされます。

#[cfg(feature = "anyhow")]
#[async_trait]
impl Writer for ::anyhow::Error {
    async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
        res.render(StatusError::internal_server_error());
    }
}

カスタムエラータイプの場合、必要に応じて異なるエラーページを出力できます。

use salvo::anyhow;
use salvo::prelude::*;

struct CustomError;
#[async_trait]
impl Writer for CustomError {
    async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
        res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
        res.render("custom error");
    }
}

#[handler]
async fn handle_anyhow() -> Result<(), anyhow::Error> {
    Err(anyhow::anyhow!("anyhow error"))
}
#[handler]
async fn handle_custom() -> Result<(), CustomError> {
    Err(CustomError)
}

#[tokio::main]
async fn main() {
    let router = Router::new()
        .push(Router::new().path("anyhow").get(handle_anyhow))
        .push(Router::new().path("custom").get(handle_custom));
    let acceptor = TcpListener::new("127.0.0.1:8698").bind().await;
    Server::new(acceptor).serve(router).await;
}

Handlerトレイトを直接実装

use salvo_core::prelude::*;
use crate::salvo_core::http::Body;

pub struct MaxSizeHandler(u64);
#[async_trait]
impl Handler for MaxSizeHandler {
    async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
        if let Some(upper) = req.body().and_then(|body| body.size_hint().upper()) {
            if upper > self.0 {
                res.render(StatusError::payload_too_large());
                ctrl.skip_rest();
            } else {
                ctrl.call_next(req, depot, res).await;
            }
        }
    }
}