Skip to content
fandhe-backend
GitHub

同期 3 拡張点契約リファレンス(Middleware / UpgradeHandler / RequestGate)

1. 目的と位置づけ

本書は fandhe-backend-core の extension モジュールが公開する 3 種の拡張点 trait(Middleware / UpgradeHandler / RequestGate)の全体像・契約・呼び出しタイミングを俯瞰する読み物である。個々のシグネチャ・doc test を含む一次情報源は rustdoc(crates/core/src/extension.rs の doc comment)であり、本書と記述が食い違う場合は rustdoc を正とする。

  • 設計原則「拡張点は 4 種 trait に集約」の実体のうち、extension モジュールが公開する同期 3 trait(Middleware / UpgradeHandler / RequestGate)を扱う。4 種目の Interceptor(interceptor モジュール、リダイレクト・確定済みレスポンスの改変を担う feature ゲート不要のレスポンダ系シーム)は本書のスコープ外であり、./interceptor-api.md を参照する。プラグイン(crates/plugin-*)はこれらの trait を実装する側であり、コアがプラグイン固有シンボルに依存することはない
  • 3 trait はクレート直下にも re-export される(fandhe_backend_core::Middleware 等)
  • trait 自体は feature によらず無条件で公開されるが、実装ゼロなら実行時コストもゼロ(pay-for-what-you-use に反しない)
  • 登録は Server::middleware / Server::gate / Server::upgrade_handler(server-api.md 参照)
  • 自作手順・実装例は ../guide/extension-points.md を参照

2. 公開 API 一覧

2.1 Middleware — 観測専用フック

メソッドシグネチャ概略説明
namefn (&self) -> &'static str診断・ログ表示用の静的識別名
on_requestfn (&self, &RequestHead)リクエストヘッド受理後・ルーティング前に呼ばれる
on_responsefn (&self, &RequestHead, Duration)レスポンス送出後の観測フック(既定実装は no-op)。Duration は受理から送出までの経過時間。コアから直接は呼ばれず、on_response_with_status の既定実装経由で呼ばれる
on_response_with_statusfn (&self, &RequestHead, u16, Duration)レスポンス送出後に呼ばれる。u16 はクライアントへ実際に送出したステータス(Interceptor::map_response 等の改変適用後の最終値)。コアが呼ぶのは本フックのみで、既定実装は status を無視して on_response へ委譲する

ロギング・メトリクス等の横断的関心事向け。レスポンスへの参照を持たない(on_response / on_response_with_status の引数はリクエストヘッド・送出ステータスの数値・経過時間のみ)。レスポンスの読み取り・書き換えはできない。

  • ステータス付きアクセスログ(GET /path 200 3ms)が必要なら on_response_with_status を override する。実行可能な例は Middleware の rustdoc「ステータス付きアクセスログの例」を参照
  • on_response だけを実装した既存コードは変更不要でそのまま動作する
  • 両方を override した場合、on_response はコアから呼ばれなくなる
  • 呼ばれるのは応答が完走した場合のみ(Upgrade 委譲・委譲失敗時の 501・書き込み失敗・ストリーミング打ち切り・パースエラー応答では呼ばれない)

2.2 UpgradeHandler — 長時間接続への委譲判定

メソッドシグネチャ概略説明
namefn (&self) -> &'static str診断・ログ表示用の静的識別名
matchesfn (&self, &RequestHead) -> boolこのリクエストが自分の担当するアップグレードプロトコルに該当するかを判定

判定のみの拡張点。matches が true を返すと以降の接続処理はプラグイン側へ委譲され、フレーミング・プロトコルアップグレード後の読み書きは本 trait の責務外(crates/plugin-websocket 等に閉じる)。

2.3 RequestGate — 早期拒否ゲート

メソッドシグネチャ概略説明
namefn (&self) -> &'static str診断・ログ表示用の静的識別名
checkfn (&self, &RequestHead, &GateContext) -> GateOutcomeリクエストヘッドと接続コンテキストを検査し許可/拒否を判定

ctx: &GateContext(BREAKING CHANGE)は accept したソケットの実 peer address を GateContext::peer_addr() -> Option<SocketAddr> として提供する。crate::handle_connection(tokio::io::duplex 等の非ソケット経路を含む)からの呼び出しでは実 peer が存在しないため None になる。peer address を判定に用いる gate 実装は、None の場合も必ず GateOutcome::Reject を返すこと(フェイルクローズ、.claude/rules/security.md)。リバースプロキシ配下では peer_addr はプロキシ自身のアドレスになる点にも注意する(実 peer を注入したい呼び出し元向けの公開 API は handle_connection_with_peer_addr)。

2.4 GateOutcome — 判定結果

variantフィールド説明
Allowなし許可。以降の処理(ルーティング等)を続行
Rejectresponse: Response拒否。検証済みの Response(crates/http)をそのまま応答として送出する

許可/拒否の判定結果のみを運び、JWT クレーム・org_id 等のプラグイン固有データをコアへ持ち込まない。Reject の response は status: u16 / body: Vec<u8> の個別フィールドではなく、検証済み Response を直接運ぶ形になっている。Response の構築時検証(CR/LF/NUL 拒否・Content-Length/Connection/ Transfer-Encoding の予約名拒否)を経た値のみが渡せるため、任意文字列を無検証でヘッダ・ステータス行へ書き出す経路は存在しない(レスポンス分割・ヘッダインジェクション対策)。ヘッダなしの従来相当の拒否応答は GateOutcome::reject(status, body) ヘルパで構築でき、Retry-After 等ヘッダ付き拒否応答(レート制限等)は Response::with_header で組み立ててから Reject { response } へ渡す。

3. 呼び出しタイミング比較

コアのリクエスト処理パイプライン(1 リクエストあたり)における評価位置。

順序ステップ拡張点備考
1Middleware::on_requestMiddleware登録順に全件呼び出し
2RequestGate::checkRequestGate登録順に評価、最初の Reject を優先。拒否時は以降のステップへ進まない
3UpgradeHandler::matchesUpgradeHandler登録順に評価。マッチしたら接続ごとプラグインへ委譲(以降のステップなし)
4パスインターセプト型プラグイン(拡張点外)WebRTC・GraphQL・OpenAPI・静的配信等。Some(response) なら 5 をスキップ
5Handler::handle(既定ハンドラ)未登録時は 404
6レスポンス後処理型プラグイン(拡張点外)CORS ヘッダ付与 → gzip 圧縮の順で逐次適用(4・5 双方の応答が対象)
7レスポンス書き込み → Middleware::on_response_with_statusMiddleware登録順に全件呼び出し。既定実装が on_response へ委譲するため、on_response のみ実装した Middleware も従来どおり呼ばれる
観点MiddlewareUpgradeHandlerRequestGate
目的観測(ロギング・計測)長時間接続への委譲判定早期拒否(認証・認可・同意)
戻り値なし(副作用のみ)boolGateOutcome
リクエストへの影響なし(変更禁止契約)true で接続ごと委譲Reject で即時拒否応答
レスポンスへのアクセスなし(送出ステータス・経過時間のみ)なし拒否応答の status/body を自ら生成
複数登録時全件呼び出し(登録順)最初のマッチで確定最初の Reject で確定

RequestGate を UpgradeHandler・パスインターセプト型より先に評価するのは、認可ゲートの既定拒否が WebSocket アップグレードやプラグイン応答にも漏れなく及ぶようにするため。ゲートを迂回してプラグインへ到達する経路は存在しない(バイパス不可)。

4. 契約・不変条件

  1. 3 trait とも同期 API: async fn を持ち込むと Box<dyn Middleware> 等の trait object 保持(dyn 互換性)が壊れるため。既定ハンドラ Handler::handle のみ async という非対称設計(server-api.md 参照)。
  2. Send + Sync 必須: 実装は Arc<Server> 経由で複数コネクションタスクから共有参照される。
  3. Middleware は同期ブロッキング I/O 禁止: 実測でスループットが最大 25% 劣化する。ロギング等で I/O が必要な実装は非同期チャネルへの送信に留め、実際の I/O は別タスクで行う(詳細規約は AGENTS.md)。crates/plugin-tracing が本契約に従う参照実装(非同期・バッファ済み writer)。
  4. Middleware は head を変更しない: 観測専用の契約。型では強制されないため実装者が守る規約として明記されている。
  5. name() に機密を含めない: 静的識別名であり、リクエスト内容(トークン・PII)を含めてはならない。
  6. RequestGate はフェイルクローズ: 判定に必要な情報が欠落・不正・判定不能な場合は必ず GateOutcome::Reject を返す(疑わしきは通過させない)。
  7. UpgradeHandler は判定のみ: matches はヘッダ検査等の軽量判定に留める。ハンドシェイク検証・フレーミングはプラグイン側の責務。

5. セキュリティ観点

  • ゲート最優先・バイパス不可: RequestGate は Upgrade・パスインターセプトを含むすべての応答生成経路より先に評価される。認可の既定拒否をコアの評価順序で保証する。
  • フェイルクローズ: 情報欠落時に Allow へ倒す実装は契約違反。複数ゲート登録時も最初の Reject が優先され、後段ゲートの Allow で覆せない。
  • インジェクション耐性: GateOutcome::Reject の status: u16 設計により、拒否応答のステータス行へ任意文字列が流入する経路を型レベルで排除している。
  • 観測ログの機密混入防止: Middleware 実装はログに機密(トークン・PII)を出さないこと。name() も同様。

6. スコープ外・関連ドキュメント

  • 拡張点に載らないプラグイン配線は利用者実装不可: パスインターセプト型(try_intercept)・レスポンス後処理型(finalize_response)・Upgrade 委譲の実体(try_handle_upgrade)は、いずれもコアの非公開 plugin モジュール内のシームであり、公開拡張点ではない。利用者が独自プラグインをこれらのシームへ配線することはできない(設計判断は docs/design/plugin-boundary.md を参照)。利用者が実装できる拡張点は本書の 3 trait・Interceptor(本書スコープ外、./interceptor-api.md 参照)・Handler trait に限られる。
  • サーバへの登録 API・リクエスト処理全体の設定: server-api.md
  • RequestHead の読み取り API(ヘッダ・パス・クエリ・Cookie): http-api.md
  • 4 拡張点の自作ガイド・実装例: ../guide/extension-points.md
  • feature 構成別サンプル: ../guide/feature-samples.md
  • 一次情報源(rustdoc): crates/core/src/extension.rs(GitHub: https://github.com/Fandhe-AI/fandhe-backend/blob/main/crates/core/src/extension.rs)