warpのFilter定義

warpはRustのWebサーバーフレームワークの1つです。
gRPCフレームワークの Tonicと同じhyperの上に構築されています。

warpの特徴は、 Filterを用いたルーティング定義です。

従来のWebフレームワークは、Config定義でルーティングするものが主流ですが、warpは関数の組み合わせで定義します。

トップレベル定義

フィルタの関数の定義方法は自由ですが、ルートが増えてくると構造が分かりにくくなるため、個別の定義は別関数に切り出す構成が多くなるでしょう。

パスごとにルート定義する場合、以下のような構造になります。

fn routes() -> impl Filter<Extract = impl warp::Reply, Error = warp::Rejection> + Clone {

    warp::get()
        .and(page1())
        .or(page2())
        .or(page3())
        .or(page4())
        .or(page5())
        .recover(handle_rejection)
}

page1()からpage5()はサブフィルタとして定義します。
各フィルタの最終処理でそれぞれimpl warp::Reply(HTTPレスポンスを表すトレイト)を返す構造をとるとトラブルが少ないでしょう。

recover()Err<warp::Rejection>を受け取った際の異常系レスポンスを処理するフィルタです。
一般的なResult型のコンビネータと同様に、right-hand sideの処理を集約できます。

recover()の使い方については、 サンプルコードhandle_rejection()の実装が参考になります。

ルートが増えてきた場合、トップレベルの定義を別の関数に分割できます。
その際の注意点は、戻り型をトップレベルに合わせることです。後述のBoxedFilterを返す関数では型が合わないケースがあります。

サブフィルタ

サブフィルタは、上流で該当条件を限定するフィルタを書き、下流でレスポンスを返すフィルタを書きます。

この例ではトップレベルのprefixと合わせて、Rails風に書けば/app/page1/:nameというpathにマッチするリクエストをハンドルしています。

マッチしない場合には、page1()の下流ハンドラは適用されず、or()で併記した別のサブフィルタでチェックされます。

なお、or()は上から順に評価されるため、条件の緩いフィルタを上に置くとより厳しいフィルタに到達しないので注意しましょう。
また、path()関数にはend()関数を併用しないと、複数のpathにマッチして意図せず複数の処理を実行することがあります。 いずれかの関数を実行しているからといって他の関数を実行していないとは言えず、想定外のルートが適用されていると切り分けに手間どります。

後半のレンダリングでは、上流でキャプチャしたnameを引数にとる関数を定義しています。
この例では変数は1つですが、キャプチャした変数の数と引数のアリティは一致する必要があります。

fn page1() -> BoxedFilter<(impl warp::Reply, )> {
    let prefix = warp::path!("app" / ..);

    prefix
        .add(warp::path!("page1" / String))
        .map(render_page1)
        .boxed()
}

fn render_page1(name: String) -> (warp::reply::Html<String>, ) {
        (warp::reply::html(format!("<html><body><h1>Hello, {}!</h1></body></html>", name), )
}

boxed()は、BoxedFilter型を返すメソッドで、おそらく多用することになります。

なお、ハンドラに処理を渡すコンビネータは、Result型を踏襲しているため、 Result型のコンビネータに習熟しておくと良いでしょう。

map()and_then()などがあります。 and_then()はエラーを返すことがあるケースで利用します。

map()and_then()では戻り値の型が異なり、and_then()はTryFuture型を返す必要があるため、以下のようなシグネチャになります。

async fn render_page1(name: String) -> Result<(warp::reply::Html<String>, ), warp::Rejection> {
}

なおエラー型はRejectionに固定されています。

正常処理の戻り型統一

各エンドポイントの正常処理時に返す結果は、Result<impl Reply, Rejection>impl Replyで、 replyモジュールの各関数で生成できます。

implはトレイトを実装する任意の型を指すRustの簡略記法です。
ただ、トレイトを実装するある1種類の型に定まることが必要な点は分かりづらいポイントです。戻り値implの制約を理解していないとエラーメッセージの意図を読みとりづらいでしょう。

たとえばhtml()が返すHtml型とwith_headerh()が返すWithHeader型はimpl Replyを満たすのですが、1つの関数からHtmlWithHeaderの両方を返すとコンパイルエラーになります。
つまり素朴に実装すると、HTMLページを返す場合とHTTPリダイレクトに分岐するような処理を書けません。これは別の関数に切り出しても解決せず、むしろ推論精度が落ちるケースが増えます。

ドキュメントには対処方法の記述がないのですが、impl Replyにより各Structは.into_resoponse()というメソッドを持っとおり、これを呼ぶと Response型になり、型が揃います。Responseimpl Replyを満たすため関数内の各分岐がOk(Response)を返すように統一するとコンパイルエラーを解消できます。

エラー処理

各フィルタに定義するwarpのレスポンスはResult<impl Reply, Rejection>という型です。
なお、404 NotFound403 ForbiddenなどのHTTPエラーレスポンスはOk(impl Reply)のバリアントです。

Rejectionは、例外処理のようなフロー制御のオブジェクトで、そのまま後続処理がなければUnhandledrejectionとなり500 InternalServerErrorを返します。

多くのアプリケーションは、500エラーを返すケースを限定したいはずです。
そこで、 フィルタrecover()メソッドを用いてOk(warp::reply::with_status(warp::reply(), warp::http:StatusCode:NOT_FOUND))のような値を返します。

充実したフィルタ

上の例では分かりやすさのためパス一致に絞りましたが、warpは ビルトイン・フィルタが充実しており、HTTPメソッドやHTTPヘッダー、POSTのJSONリクエストボディなど多彩なパラメータを条件に利用できます。

また、レンダリングに用いるパラメータもRust関数で任意の加工が可能であるため、ルーティングが複雑になるほど実力を発揮します。

ものすごく複雑なロジックを書いたとしても、warp上で動作させる部分についてはきわめて高速かつ省リソースで処理できるはずです。
この点がwarpのハイライトと言えます。

各フィルタ実装のポイントとして、極力パスの判定を最初に終える(冒頭に定義する)ことが重要です。
一般的にパスごとに処理が異なるため、リクエストが適切なパスにルートされることが暗黙の前提になります。
この点を徹底しておかないと、どのフィルタに処理が渡っているのかが分からなくなり、デバッグが迷宮入りします。

ポイント

warpはルーティング・プロセスでリクエストからパラメータを柔軟に抽出する機能に特化したフレームワークです。
適切な設計には、まず理解度をひき上げることが有効です。

  • warp::Filterのドキュメントにフィルタの設計意図が多少書かれているため、理解の補助に使える
  • フィルタのコンビネータはResultと似ている。エラー型の主役は Rejectionとそのための rejectモジュール
  • 戻り値の型が分かりにくいため、サブフィルタの関数が何を返すべきなのかに注意。コンパイルエラーが出る場合、コンパイラが検出した型をシグネチャに流用するのも一手
  • とくに正常系のレスポンスとして、タプルを常用している点が独特
  • パス判定が最重要。path()path!()は挙動が異なる
  • warp::post()などのビルトインフィルタは特定のメソッドのみに対応するため一見便利だが、405 Method Not Allowedを返すためアタックに対するヒントが増えてしまう。GET/POSTで2つのAPIを提供する場合以外は避けた方が良い

組み上げるまでの設計体力は問われますが、努力したなりの堅牢性と保守性は期待できそうです。

なお、冒頭で述べたとおりwarpとtonicは同じhyper上に構築されていますが、APIの共通性は一切なく同一プロセス上で動作させるわけでもないため、セットで考える意味合いはほとんどないでしょう。

⁋ 2021/09/13↻ 2024/11/07
中馬崇尋
Chuma Takahiro