axum crate の Router (2) 設定済みの Router はどう動くのか
前回は axum crate の Router (1) で Router の route メソッドなどを見ました。 なんとなく Router の内部構造をつかみはじめたところです。
今回は fallback を見ていくと書いたのですが、すこし読んでみたところ、設定済みの Router がどのように動くのかを把握するほうが良さそうだと考えたので、そちらを確認していきます。
今回も axum crate のバージョンは 0.8.6 です。
serve からはじめる
前々回の axum crate のかんたんな例 の中で、 serve の例がありました。再掲します。
#[tokio::main]
async fn main() {
let app = axum::Router::new()
.route("/", axum::routing::get(root))
.route("/users", axum::routing::post(create_user))
.route("/users/{user_id}", axum::routing::get(get_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Router を serve の第二引数に指定し、サーバーを起動していました。ここを今回の出発点にします。
serve のシグネチャと make_service
https://docs.rs/axum/0.8.6/axum/fn.serve.html
pub fn serve<L, M, S>(listener: L, make_service: M) -> Serve<L, M, S>
where
L: Listener,
M: for<'a> Service<IncomingStream<'a, L>, Error = Infallible, Response = S>,
S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + 'static,
S::Future: Send,
serve は思ったよりも複雑なシグネチャを持っています。
Router は、第二引数に指定できたことから M のトレイト境界 for<'a> Service<IncomingStream<'a, L>, Error = Infallible, Response = S> を満たしているはずです。
これはどういうことでしょう。 Service は tower_service crate の Service 。つまり tower::Service です。
https://docs.rs/tower-service/0.3.3/tower_service/trait.Service.html
IncomingStream は axum::serve::IncomingStream です。
https://docs.rs/axum/0.8.6/axum/serve/struct.IncomingStream.html
Error = Infallible の Infallible は std::convert::Infallible です。これは決して失敗しない (起こらない) エラーのことですね。
https://doc.rust-lang.org/std/convert/enum.Infallible.html
Response = S の S はトレイト境界から HTTP リクエストを受け取って HTTP レスポンスを返すサービスです。
ややこしいですが、ここまでで Router (M) は Service を返す Service であることが分かります。
Router は Service で IncomingStream を受け取って Service を返します。ここで返された Service は Request を受け取って Response を返す Service ということです。
ああ、ややこしい。
impl Service<IncomingStream, ...> for Router<()>
さて、 Router が Service<IncomingStream, ...> を実装している箇所を探します。
impl<L> Service<IncomingStream<'_, L>> for Router<()> は Router のドキュメントの Trait Implementations から見つけられます。
https://docs.rs/axum/0.8.6/axum/struct.Router.html#impl-Service<IncomingStream<'_,+L>>-for-Router
ドキュメントにも記載されていますが、 Router<()> のように Router<S> の S は () である必要があります。すべての state が欠けていない Router が要求されているということですね。
実装は次のとおりです。
https://docs.rs/axum/0.8.6/src/axum/routing/mod.rs.html#549-566
impl<L> Service<serve::IncomingStream<'_, L>> for Router<()>
where
L: serve::Listener,
{
type Response = Self;
type Error = Infallible;
type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: serve::IncomingStream<'_, L>) -> Self::Future {
// call `Router::with_state` such that everything is turned into `Route` eagerly
// rather than doing that per request
std::future::ready(Ok(self.clone().with_state(())))
}
}
ready やらで wrap されていますが、 self.clone().with_state(()) したものを返しています。
with_state(()) は Router<()> を返します。
https://docs.rs/axum/0.8.6/axum/struct.Router.html#method.with_state
つまり、 Router を with_state(()) したものは serve の S (HTTP リクエストを受け取って HTTP レスポンスを返すサービス) を満たすことが分かります。
再掲: serve の S 。
pub fn serve<L, M, S>(listener: L, make_service: M) -> Serve<L, M, S>
where
L: Listener,
M: for<'a> Service<IncomingStream<'a, L>, Error = Infallible, Response = S>,
S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + 'static,
S::Future: Send,
impl Service<Request, ...> for Router
また Service の実装を探します。今度は Request を受け取って Response を返す Service の Router のための実装です。
impl<B> Service<Request<B>> for Router<()> も Router のドキュメントから Trait Implementations から見つけられます。
https://docs.rs/axum/0.8.6/axum/struct.Router.html#impl-Service<Request<B>>-for-Router
実装は次のようになっています。
https://docs.rs/axum/0.8.6/src/axum/routing/mod.rs.html#569-588
impl<B> Service<Request<B>> for Router<()>
where
B: HttpBody<Data = bytes::Bytes> + Send + 'static,
B::Error: Into<axum_core::BoxError>,
{
type Response = Response;
type Error = Infallible;
type Future = RouteFuture<Infallible>;
#[inline]
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
#[inline]
fn call(&mut self, req: Request<B>) -> Self::Future {
let req = req.map(Body::new);
self.call_with_state(req, ())
}
}
axum::body::Body::new で http::Request<B> を map し、 Router::call_with_state を呼び出しています。
ようやく HTTP リクエストを処理している箇所にたどりつきそうです。
Router::call_with_state
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)
}
来ましたね。
self.inner.path_router.call_with_state を呼び出し、成功すれば返して終了、失敗すれば次へ。
self.inner.fallback_router.call_with_state を呼び出し、成功すれば返して終了、失敗すれば次へ。
self.inner.catch_all_fallback.call_with_state を呼び出す。
前回は分からなかった RouterInner のフィールド がなんとなく見え、当初今回のゴールに設定していた fallback も近づいてきましたね。
PathRouter::call_with_state
今回は PathRouter を見て終わりにしましょう。
https://github.com/tokio-rs/axum/blob/axum-v0.8.6/axum/src/routing/path_router.rs#L371-L420
pub(super) fn call_with_state(
&self,
#[cfg_attr(not(feature = "original-uri"), allow(unused_mut))] mut req: Request,
state: S,
) -> Result<RouteFuture<Infallible>, (Request, S)> {
#[cfg(feature = "original-uri")]
{
use crate::extract::OriginalUri;
if req.extensions().get::<OriginalUri>().is_none() {
let original_uri = OriginalUri(req.uri().clone());
req.extensions_mut().insert(original_uri);
}
}
let (mut parts, body) = req.into_parts();
match self.node.at(parts.uri.path()) {
Ok(match_) => {
let id = *match_.value;
if !IS_FALLBACK {
#[cfg(feature = "matched-path")]
crate::extract::matched_path::set_matched_path_for_request(
id,
&self.node.route_id_to_path,
&mut parts.extensions,
);
}
url_params::insert_url_params(&mut parts.extensions, match_.params);
let endpoint = self
.routes
.get(&id)
.expect("no route for id. This is a bug in axum. Please file an issue");
let req = Request::from_parts(parts, body);
match endpoint {
Endpoint::MethodRouter(method_router) => {
Ok(method_router.call_with_state(req, state))
}
Endpoint::Route(route) => Ok(route.clone().call_owned(req)),
}
}
// explicitly handle all variants in case matchit adds
// new ones we need to handle differently
Err(MatchError::NotFound) => Err((Request::from_parts(parts, body), state)),
}
}
今回の引用では最長ですね。
ざっくりと見ると次のようになっています。
-
req.into_parts()で HTTP ボディとそれ以外に分けて -
self.node.at(parts.uri.path())でマッチするルート (のID) を探して - あれば
self.routes.get(&id)でEndpointを得て、なければ Err -
EndpointがMethodRouterならcall_with_state、ネストしたRouteならcall_ownedでさらに呼び出し
Node::at
self.node.at (Node::at) を見ると、 self.inner.at に丸投げされています。
https://github.com/tokio-rs/axum/blob/axum-v0.8.6/axum/src/routing/path_router.rs#L501-L506
fn at<'n, 'p>(
&'n self,
path: &'p str,
) -> Result<matchit::Match<'n, 'p, &'n RouteId>, MatchError> {
self.inner.at(path)
}
Node::inner は matchit crate の Router です。
https://github.com/tokio-rs/axum/blob/axum-v0.8.6/axum/src/routing/path_router.rs#L478-L482
struct Node {
inner: matchit::Router<RouteId>,
route_id_to_path: HashMap<RouteId, Arc<str>>,
path_to_route_id: HashMap<Arc<str>, RouteId>,
}
matchit は axum crate とは別のクレートです。
こちらの詳細は別の機会に読むことにします。
残りの処理
PathRouter::call_with_state に戻り、 Node::at でマッチしたルートの value (RouteId) と params を得たところから見ます。
得られた id: RouteId で Router::routes から Endpoint を得ます。 self.routes.get(&id) ですね。
Endpoint の variant で分岐して、 MethodRouter::call_with_state と Route::call_owned を呼び分けます。
そして…… MethodRouter 以降は……省略! 力尽きました!
このあとは Route::oneshot_inner_owned を呼び出して、 tower::util::BoxCloneSyncService::oneshot の呼び出しへとつながっていくようです。ここはまたハンドラーかミドルウェアのタイミングでまた出てくると思います。
おわりに
今回は axum crate の学び直しの第 3 回として、設定した Router がどう動くのかを追いかけてみました。
Router が Service を返す Service であり、そこで返された Service は Request を受けて Response を返すものだと分かりました。
Router::call_with_state は Router のフィールドにある path_router, fallback_router, catch_all_fallback へと処理を転送していました。
path_router (PathRouter) は設定されたルート (Node) から matchit crate でマッチするものを選んで、 MethodRouter などにさらに処理を転送していました。
MethodRouter からは Route に設定されたサービスの呼び出しにつながっていきそうな点を見ることができました。
Discussion