从零写一个 mini-axum
主线位置:两条主线都自己走一遍。装配主线:宏让
async fn实现Handler<T, S>,Router::route把它擦成一个Arc<dyn Fn>存进列表;请求主线:Service::call把 body 换成统一类型,按方法和路径找到端点,handler 依次跑提取器、调用函数、into_response。
flowchart TB fnitem["async fn handler(a, b)"] -->|"impl_handler!"| h["Handler (M, A, B), S"] h -->|"Router::route"| erased["Arc dyn Fn:擦掉 H 与 T"] erased --> list["Vec Endpoint"] list --> svc["tower Service"] svc -->|TowerToHyperService| hyp["hyper http1 连接"] req["Request B"] --> conv["body 换成统一 Body"] conv --> disp["方法 + 路径精确匹配"] disp -->|没找到| nf[404] disp --> parts["前面的参数:FromRequestParts"] parts --> last["最后一个:FromRequest S M"] last --> callfn["调用函数,into_response"]
本页只讲思路和关键代码。mini 是一个约 300 行的库加一个测试程序,不依赖 axum:
bytes = "1"
http = "1"
http-body = "1"
http-body-util = "0.1"
hyper = { version = "=1.12.0", features = ["server", "client", "http1"] }
hyper-util = { version = "=0.1.21", features = ["tokio", "service"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "=1.54.1", features = ["rt-multi-thread", "macros", "net"] }
tower = { version = "=0.5.3", default-features = false }
hyper 的 client feature 只给测试程序用。完整实现(库、测试程序、编译错误的几个变体)、路由树、with_state 式的延迟状态和中间件的从零实现,以及配套作业,放在付费课程里。
响应:一个 body 类型,一个 trait
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub type Body = UnsyncBoxBody<Bytes, BoxError>;
pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
pub trait IntoResponse {
fn into_response(self) -> Response<Body>;
}
impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
fn into_response(self) -> Response<Body> {
let mut res = self.1.into_response();
*res.status_mut() = self.0;
res
}
}
另外给 Response<Body>、StatusCode、&'static str、String、Result<T, E>、Json<T>、Infallible 各写了一个 impl,都是几行。
请求和响应用同一个装箱的 body 类型,是整个设计的起点:只要类型固定,Handler、提取器、Router 都不用再带一个 body 类型参数。axum-core 的 Body 也是包了一层 UnsyncBoxBody<Bytes, Error>。axum 的 IntoResponse 多出来的是 IntoResponseParts 和最长 16 元的元组规则,见 IntoResponse 与 IntoResponseParts。
两个提取器 trait 和 ViaParts
pub trait FromRequestParts<S>: Sized {
type Rejection: IntoResponse;
fn from_request_parts(parts: &mut Parts, state: &S) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
}
pub trait FromRequest<S, M = ViaRequest>: Sized {
type Rejection: IntoResponse;
fn from_request(req: Request<Body>, state: &S) -> impl Future<Output = Result<Self, Self::Rejection>> + Send;
}
pub enum ViaParts {}
pub enum ViaRequest {}
// 只看头的提取器放在最后一个参数时走这里:body 直接丢掉
impl<S: Sync, T: FromRequestParts<S>> FromRequest<S, ViaParts> for T {
type Rejection = T::Rejection;
async fn from_request(req: Request<Body>, state: &S) -> Result<Self, Self::Rejection> {
let (mut parts, _body) = req.into_parts();
T::from_request_parts(&mut parts, state).await
}
}
和 axum-core 的 FromRequestParts、FromRequest 签名一致,blanket impl 也是同一个写法。axum 把两个标记放在私有模块里,外部 crate 写不出 ViaParts;mini 省了这一步。为什么前面的参数只能借 &mut Parts、body 只给最后一个,见 FromRequestParts 与 FromRequest。
mini 实现了五个提取器:Method、HeaderMap、State<S>(克隆状态)走 FromRequestParts;String、Json<T> 走 FromRequest,用 BodyExt::collect 把 body 读完。Json 先看 Content-Type 是不是以 application/json 开头,不是就回 415,解析失败回 400。拒绝类型一律是 (StatusCode, String)。
Handler:宏为 0–3 个参数生成 impl
pub trait Handler<T, S>: Clone + Send + Sync + 'static {
fn call(self, req: Request<Body>, state: S) -> BoxFuture<Response<Body>>;
}
macro_rules! impl_handler {
([$($ty:ident),*], $last:ident) => {
#[allow(non_snake_case, unused_mut)]
impl<F, Fut, Res, S, M, $($ty,)* $last> Handler<(M, $($ty,)* $last,), S> for F
where
F: FnOnce($($ty,)* $last) -> Fut + Clone + Send + Sync + 'static,
Fut: Future<Output = Res> + Send,
Res: IntoResponse,
S: Send + Sync + 'static,
$( $ty: FromRequestParts<S> + Send, )*
$last: FromRequest<S, M> + Send,
{
fn call(self, req: Request<Body>, state: S) -> BoxFuture<Response<Body>> {
let (mut parts, body) = req.into_parts();
Box::pin(async move {
$(
let $ty = match $ty::from_request_parts(&mut parts, &state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
)*
let req = Request::from_parts(parts, body);
let $last = match $last::from_request(req, &state).await {
Ok(value) => value,
Err(rejection) => return rejection.into_response(),
};
self($($ty,)* $last).await.into_response()
})
}
}
};
}
impl_handler!([], T1);
impl_handler!([T1], T2);
impl_handler!([T1, T2], T3);
0 个参数的函数没有「最后一个参数」,单独写一个 Handler<((),), S> 的 impl,这里从略。
拿它和 axum 的 impl_handler! 逐行对,几乎一样:T 是 (M, T1, ..., Tn),前面的参数要求 FromRequestParts<S>,最后一个要求 FromRequest<S, M>;拆开请求、依次提取、失败就把 rejection 变成响应返回、拼回 Request 交给最后一个、调用函数。#[allow(non_snake_case, unused_mut)] 也是照抄:变量名就是类型参数名 T1,0 个前置参数时 parts 用不到 mut。不同的地方只有三处:
| mini | axum 0.8.9 | |
|---|---|---|
| future 类型 | call 直接返回 BoxFuture | Handler 有关联类型 type Future,宏生成的 impl 把它定为 Pin<Box<dyn Future<Output = Response> + Send>>,非函数的 handler(如 Layered)可以给出自己的类型 |
| 参数个数 | 宏调用 3 次,最多 3 个参数 | all_the_tuples! 调 16 次,第 17 个参数编译不过,见 Handler trait |
| 报错 | 无 | #[diagnostic::on_unimplemented] 的提示和每个 impl 上的 #[diagnostic::do_not_recommend],编译不过时报错更短,见常见坑 |
问: 每个提取器都写 let x = match ... { Ok(v) => v, Err(r) => return ... };,啰嗦。直接在 Ok 分支里调用函数不行吗?
match T1::from_request(req, &state).await {
Ok(t1) => self(t1).await.into_response(),
Err(rejection) => rejection.into_response(),
}
答: 不行。match 的被匹配值是一个 Result<T1, Rejection> 临时值,它要活到整个 match 结束,于是跨过了 self(t1).await。Rejection 只要求 IntoResponse,没有要求 Send,整个 future 就不能证明是 Send,装不进 BoxFuture。把单参数的 impl 改成这样,rustc 1.99.0 报:
error: future cannot be sent between threads safely
--> src/bin/send_match.rs:19:5
|
19 | / Box::pin(async move {
20 | | // 在 Ok 分支里直接 await 用户函数
21 | | match T1::from_request(req, &state).await {
22 | | Ok(t1) => self(t1).await.into_response(),
... |
25 | | })
| |______^ future created by async block is not `Send`
|
= help: within `{async block@src/bin/send_match.rs:19:14: 19:24}`, the trait `Send` is not implemented for `<T1 as FromRequest<S, M>>::Rejection`
note: future is not `Send` as this value is used across an await
--> src/bin/send_match.rs:22:28
|
21 | match T1::from_request(req, &state).await {
| ----------------------------------- has type `Result<T1, <T1 as FromRequest<S, M>>::Rejection>` which is not `Send`
22 | Ok(t1) => self(t1).await.into_response(),
| ^^^^^ await occurs here, with `T1::from_request(req, &state).await` maybe used later
= note: required for the cast from `Pin<Box<{async block@src/bin/send_match.rs:19:14: 19:24}>>` to `Pin<Box<dyn Future<Output = Response<_>> + Send>>`
let ... = match ...; 让临时值在分号处就被丢掉,后面的 await 只持有 t1。axum 宏里的写法也是这样。
去掉标记类型:E0207
宏里的 M 只在 $last: FromRequest<S, M> 里用到。如果嫌它碍眼,把 trait 参数从 (M, $($ty,)* $last,) 改成 ($($ty,)* $last,),其他一个字都不动:
// 和 mini 的唯一区别:T 里没有 M
impl<F, Fut, Res, S, M, $($ty,)* $last> Handler<($($ty,)* $last,), S> for F
rustc 1.99.0(展开两次,报两次,第二条只差调用位置):
error[E0207]: the type parameter `M` is not constrained by the impl trait, self type, or predicates
--> src/bin/no_marker.rs:14:26
|
14 | impl<F, Fut, Res, S, M, $($ty,)* $last> Handler<($($ty,)* $last,), S> for F
| ^ unconstrained type parameter
...
44 | impl_handler!([], T1);
| --------------------- in this macro invocation
|
= note: this error originates in the macro `impl_handler` (in Nightly builds, run with -Z macro-backtrace for more info)
impl 的每个类型参数都必须出现在 trait 引用或 self 类型里(或者被它们通过关联类型确定)。M 只出现在 where 子句里一个 trait 的泛型参数位置,T1 确定了也定不下 M:同一个 T1 可以同时实现 FromRequest<S, ViaParts> 和 FromRequest<S, ViaRequest>。Handler trait 一页用一个只依赖 std 的玩具 trait 展示了同一个错误,那里是参数类型 A 没被约束;这里是 M,原因相同,补法也相同:把它放进 T。
不要标记、写两个 impl:E0119
另一种想法:不要 M,最后一个参数要么是 FromRequestParts、要么是 FromRequest,各写一个 impl:
impl<F, Fut, Res, S, T1> Handler<(T1,), S> for F
where
// ...
T1: FromRequestParts<S> + Send,
{
// ...
}
impl<F, Fut, Res, S, T1> Handler<(T1,), S> for F
where
// ...
T1: FromRequest<S> + Send,
{
// ...
}
rustc 1.99.0:
error[E0119]: conflicting implementations of trait `Handler<(_,), _>`
--> src/bin/overlap.rs:31:1
|
11 | / impl<F, Fut, Res, S, T1> Handler<(T1,), S> for F
12 | | where
13 | | F: FnOnce(T1) -> Fut + Clone + Send + Sync + 'static,
14 | | Fut: Future<Output = Res> + Send,
15 | | Res: IntoResponse,
16 | | S: Send + Sync + 'static,
17 | | T1: FromRequestParts<S> + Send,
| |_________________________________- first implementation here
...
31 | / impl<F, Fut, Res, S, T1> Handler<(T1,), S> for F
32 | | where
33 | | F: FnOnce(T1) -> Fut + Clone + Send + Sync + 'static,
34 | | Fut: Future<Output = Res> + Send,
35 | | Res: IntoResponse,
36 | | S: Send + Sync + 'static,
37 | | T1: FromRequest<S> + Send,
| |____________________________^ conflicting implementation
两个 impl 的头完全相同,只有 where 不同。一致性检查只问「有没有一个类型能同时满足两边」:没有任何规则禁止一个类型既实现 FromRequestParts<S> 又实现 FromRequest<S>,所以判定重叠。FromRequestParts 与 FromRequest 一页的 E0119 发生在提取器那一层(blanket impl 和 Result 的 FromRequest impl 重叠);这里是同一个问题落在 Handler 上。
mini 的做法是把「最后一个参数走哪条路」从两个 impl 挪到一个类型参数上:只有一个 Handler impl,它要求 $last: FromRequest<S, M>;FromRequestParts 类型经由 blanket impl 得到 FromRequest<S, ViaParts>,body 提取器自己实现 FromRequest<S, ViaRequest>。M 不同,trait 就不同,不再重叠;M 又在 T 里,不会触发 E0207。调用方从不写 M,编译器对每个具体的最后一个参数推出唯一成立的那个。
Router:擦掉类型,按方法加路径找
type ErasedHandler<S> = Arc<dyn Fn(Request<Body>, S) -> BoxFuture<Response<Body>> + Send + Sync>;
struct Endpoint<S> {
method: Method,
path: String,
handler: ErasedHandler<S>,
}
pub struct Router<S> {
endpoints: Arc<Vec<Endpoint<S>>>,
state: S,
}
impl<S: Clone + Send + Sync + 'static> Router<S> {
pub fn new(state: S) -> Self {
Router { endpoints: Arc::new(Vec::new()), state }
}
pub fn route<H, T>(mut self, method: Method, path: &str, handler: H) -> Self
where
H: Handler<T, S>,
{
// 擦掉 H 和 T:从这里起,所有 handler 是同一种类型
let handler: ErasedHandler<S> = Arc::new(move |req, state| handler.clone().call(req, state));
Arc::make_mut(&mut self.endpoints).push(Endpoint { method, path: path.to_owned(), handler });
self
}
fn dispatch(&self, req: Request<Body>) -> BoxFuture<Response<Body>> {
let found = self
.endpoints
.iter()
.find(|e| e.method == req.method() && e.path == req.uri().path());
match found {
Some(e) => (e.handler)(req, self.state.clone()),
None => Box::pin(async { StatusCode::NOT_FOUND.into_response() }),
}
}
}
(Endpoint 和 Router 的 Clone 是手写的,从略。)
- 擦类型:
route是装配主线上T最后出现的地方。闭包捕获handler,类型里还有H,但一进Arc<dyn Fn>就只剩签名Fn(Request<Body>, S) -> BoxFuture<Response<Body>>。不同签名的 handler 由此能放进同一个Vec。axum 在这一步用的是BoxedIntoRoute,状态给了之后变成Route(一个BoxCloneSyncService),见路由与装箱。 - 每个请求克隆一次 handler:
Handler::call按值消费self,所以闭包里先clone。axum 的HandlerService::call也是self.handler.clone()再调用。状态同样每个请求克隆一份。 Arc::make_mut:构建时Arc只有一个持有者,原地push;如果Router已经被克隆过,才复制一份Vec。axum 的Router::into_inner用Arc::try_unwrap做同一件事,见 Router 的内部结构。- 分发:线性扫描,方法和路径都要字符串相等。没有路径参数,方法不对和路径不存在都是 404。
包成 tower Service
impl<S, B> tower::Service<Request<B>> for Router<S>
where
S: Clone + Send + Sync + 'static,
B: http_body::Body<Data = Bytes> + Send + 'static,
B::Error: Into<BoxError>,
{
type Response = Response<Body>;
type Error = Infallible;
type Future = BoxFuture<Result<Response<Body>, Infallible>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Infallible>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Request<B>) -> Self::Future {
let req = req.map(|b| b.map_err(Into::into).boxed_unsync());
let fut = self.dispatch(req);
Box::pin(async move { Ok(fut.await) })
}
}
对任何 body 类型 B 实现 Service,进门第一件事把 body 装箱成 Body。axum 的 impl Service<Request<B>> for Router<()> 也是这样,call 里一行 req.map(Body::new)。两点不同:axum 只给 Router<()> 实现(状态必须先给齐,见 State 与 FromRef),mini 的状态从 new 起就在 Router 里,所以对任何 S 都实现;axum 的 Body::new 先试 try_downcast,传进来的已经是 Body 就不再套一层盒子,mini 总是再装一次。
B0.6 及以前是 Router<S, B>、Handler<T, S, B>、FromRequest<S, B, M>。#1751(「Remove B type param」)把它们去掉,首次发布于 axum 0.7.0(axum-core 0.4.0)。PR 说明给的主要理由是 issue #1110「Middleware that change the request body have poor usability」:「If Router wasn't generic over the request body then .layer(RequestBodyLimit::new(...)) wouldn't change the router type and it would still be Router<S>.」做法正是上面这段代码的形状:「Router implements Service for any B but immediately converts that into a Body, rather than carrying the B type forward.」0.7.0 的 CHANGELOG:「The following types/traits are no longer generic over the request body (i.e. the B type param has been removed)」,列出了 FromRequestParts、FromRequest、Handler、MethodRouter、Route、Router 等 13 项。mini 一开始就固定了 Body,所以 Handler<T, S> 只有两个参数。
交给 hyper,用真实请求验证
测试程序注册五个路由,用 hyper 的 http1 服务端起在 127.0.0.1:0,每条连接用 hyper-util 的 TowerToHyperService 把 Router 转成 hyper 的 Service(它每个请求克隆一次 Router,见 Service 与 Body):
#[derive(Clone, Default)]
struct AppState {
next_id: Arc<AtomicU64>,
}
async fn root() -> &'static str {
"hello from mini\n"
}
// 最后一个参数是只看头的提取器:走 ViaParts
async fn whoami(method: Method, headers: HeaderMap) -> String {
let ua = headers.get("user-agent").and_then(|v| v.to_str().ok()).unwrap_or("-");
format!("{method} from {ua}\n")
}
async fn echo(method: Method, body: String) -> String {
format!("{method} echoed {} bytes: {body}\n", body.len())
}
async fn create_user(State(state): State<AppState>, Json(new): Json<NewUser>) -> (StatusCode, Json<User>) {
let id = state.next_id.fetch_add(1, Ordering::Relaxed) + 1;
(StatusCode::CREATED, Json(User { id, name: new.name }))
}
async fn count(State(state): State<AppState>) -> String {
format!("{} users\n", state.next_id.load(Ordering::Relaxed))
}
// main 里:
let app = Router::new(AppState::default())
.route(Method::GET, "/", root)
.route(Method::GET, "/whoami", whoami)
.route(Method::POST, "/echo", echo)
.route(Method::POST, "/users", create_user)
.route(Method::GET, "/users/count", count);
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
tokio::spawn(async move {
loop {
let Ok((stream, _)) = listener.accept().await else { break };
let svc = TowerToHyperService::new(app.clone());
tokio::spawn(async move {
if let Err(e) = hyper::server::conn::http1::Builder::new().serve_connection(TokioIo::new(stream), svc).await {
eprintln!("[srv] {e}");
}
});
}
});
NewUser { name: String } 和 User { id: u64, name: String } 是两个 serde 结构体。客户端用 hyper 的 client::conn::http1::handshake 连上去,在同一条连接上依次发 10 个请求(每个都带 user-agent: mini-client),打印状态码、content-type 和 body。M2 Max、rustc 1.99.0、--release,连跑 3 次,输出逐字相同:
GET / -> 200 OK [text/plain; charset=utf-8] b"hello from mini\n"
GET /whoami -> 200 OK [text/plain; charset=utf-8] b"GET from mini-client\n"
POST /echo -> 200 OK [text/plain; charset=utf-8] b"POST echoed 4 bytes: ping\n"
POST /users -> 201 Created [application/json] b"{\"id\":1,\"name\":\"ferris\"}"
POST /users -> 201 Created [application/json] b"{\"id\":2,\"name\":\"corro\"}"
POST /users -> 415 Unsupported Media Type [text/plain; charset=utf-8] b"expected application/json"
POST /users -> 400 Bad Request [text/plain; charset=utf-8] b"invalid JSON: missing field `name` at line 1 column 11"
GET /users/count -> 200 OK [text/plain; charset=utf-8] b"2 users\n"
DELETE / -> 404 Not Found [-] b""
GET /nope -> 404 Not Found [-] b""
- 0 个参数(
root)、ViaParts(whoami的最后一个参数HeaderMap、count唯一的参数State)、ViaRequest(echo的String、create_user的Json)三条路都走通了。三种 impl 是编译器按函数签名选的,route的调用处没有写任何类型。 State在前、Json在后:两次成功的POST /users拿到 id 1、2,状态是同一个Arc<AtomicU64>,每个请求克隆的只是Arc。- 短路:第 6 个请求的
Content-Type是text/plain,第 7 个少了name字段,分别得到 415 和 400,body 是拒绝的文本。State已经提取成功,但Json失败后return,create_user没有被调用:随后的/users/count仍是 2。 - 404:
DELETE /路径存在、方法不对,和不存在的/nope一样是 404,没有content-type。axum 在这里会回什么,是下面的问题。
vs_axum 程序只注册一个路由 GET /(handler 返回 "hello\n"),分别交给 mini 的 Router 和 axum 0.8.9 的 Router,用 tower::ServiceExt::oneshot 发四个请求:GET /、HEAD /、DELETE /、GET /nope,打印状态码、排好序的响应头和 body。这个程序额外依赖:
axum = { version = "=0.8.9", default-features = false }
tower = { version = "=0.5.3", default-features = false, features = ["util"] }
mini 的四行你已经能推出来。axum 的 HEAD / 和 DELETE / 是什么状态码?各带哪些响应头?GET / 的响应头和 mini 有什么不同?答案在本页末尾。
和 axum 比:少了什么
mini 能跑通上面这些,离 axum 还差很远。下表每一行是 mini 缺的一样东西,最后一列是本卷讲它的那一页:
| 缺的 | mini 现在怎样 | axum 怎么做 | 见 |
|---|---|---|---|
| 路由树 | Vec 线性扫描,路径逐字相等 | PathRouter 把路径插进 matchit 0.8.4 的基数树,树里只存 RouteId;支持 {id}、{*rest},冲突在启动时 panic | matchit 与路径语法、Router 的内部结构 |
| 路径参数 | 没有,也就没有 Path 提取器 | 匹配到的参数写进请求的 extensions,Path<T> 用 serde 反序列化 | Path、Query 与请求元数据 |
状态与 with_state | Router::new(state) 一开始就要状态,所有 handler 共用一个 S | Router<S> 表示「还欠着 S」,with_state 把它变成 Router<S2>;FromRef 从总状态里取子状态;只有 Router<()> 是 Service | State 与 FromRef、常见坑 |
MethodRouter 与 405 | 方法是端点的一部分,方法不对就 404;HEAD 不会落到 GET | 每个路径一个 MethodRouter,九个方法槽,HEAD 先找 head 再找 get;没有匹配的方法回 405 并带 Allow;重复注册在启动时 panic | MethodRouter |
nest、merge、fallback | 都没有,找不到就固定 404 | nest 拆开子路由、加前缀逐条注册,merge 合并两棵树,fallback 可以逐层设置 | nest、merge 与 fallback |
| 中间件 | 只能把整个 Router 套进一个 tower Layer | Router::layer 套在每个路由、405 和 fallback 上,route_layer 只套匹配到的路由,Handler::layer 套单个 handler;from_fn 写中间件 | layer 与 route_layer、from_fn 与它的兄弟 |
| 拒绝类型 | 一律 (StatusCode, String) | 每个提取器有自己的 rejection 枚举,Json 分 400、415、422;Result<T, T::Rejection> 和 Option<T> 让 handler 自己处理失败 | body 提取器、常见坑、FromRequestParts 与 FromRequest |
| body 上限 | collect 不设上限,多大都读进内存 | Bytes、String、Json 等默认最多读 2 MiB(DEFAULT_LIMIT = 2_097_152),DefaultBodyLimit 可改 | body 提取器、常见坑 |
| 装箱与响应整理 | 每个端点一个 Arc<dyn Fn>,每个请求一个 BoxFuture;响应原样返回 | 有状态前是 BoxedIntoRoute,之后是 Route(BoxCloneSyncService),每个请求克隆再 oneshot;最外层的 RouteFuture 在 poll 里补 Allow、content-length,HEAD 去掉 body | 路由与装箱 |
| 参数个数与报错 | 最多 3 个参数,签名不对就是一大段 trait 不满足 | 最多 16 个;on_unimplemented、do_not_recommend 和 #[debug_handler] 让错误指到具体参数 | Handler trait、常见坑 |
| 响应拼装 | (StatusCode, T) 一种元组 | IntoResponseParts:元组里可以混入头、扩展、状态码,最长 16 项 | IntoResponse 与 IntoResponseParts |
| 和 hyper 之间 | 自己写 accept 循环和 serve_connection | axum::serve 用 hyper-util 的 auto 构建器,同时支持 HTTP/1 和 HTTP/2,带优雅停机 | auto:一个端口两个协议、本站的服务端路径 |
前五行是路由:mini 的「方法 + 路径」是一个扁平的键,axum 把它拆成两层(路径进树、方法进 MethodRouter),405、HEAD、nest 都要靠这个分层。中间几行是对坏请求的防御和报错。Handler 和提取器这一层,mini 和 axum 的结构已经一样了:axum 多的是参数个数和诊断信息,不是机制。
动脑筋答案
vs_axum 的输出(M2 Max、rustc 1.99.0、--release,连跑 3 次,逐字相同):
mini GET / -> 200 OK ["content-type: text/plain; charset=utf-8"] b"hello\n"
axum GET / -> 200 OK ["content-length: 6", "content-type: text/plain; charset=utf-8"] b"hello\n"
mini HEAD / -> 404 Not Found [] b""
axum HEAD / -> 200 OK ["content-length: 6", "content-type: text/plain; charset=utf-8"] b""
mini DELETE / -> 404 Not Found [] b""
axum DELETE / -> 405 Method Not Allowed ["allow: GET,HEAD", "content-length: 0"] b""
mini GET /nope -> 404 Not Found [] b""
axum GET /nope -> 404 Not Found [] b""
HEAD /:axum 200,mini 404。 axum 的MethodRouter::call_with_state依次是call!(req, HEAD, head)、call!(req, HEAD, get):没有head处理函数就用get的。handler 照常返回"hello\n",RouteFuture先按 body 的size_hint设好content-length: 6,再把 body 换成空的(源码注释「make sure to set content-length before removing the body」)。mini 的端点键是(GET, "/"),HEAD对不上。DELETE /:axum 405,带allow: GET,HEAD。 路径匹配到了,MethodRouter没有delete槽,落到它自己的 fallback,Allow是注册get时拼好的(get同时写进GET和HEAD)。mini 不分路径和方法两层,只能说 404。GET /:axum 多了content-length: 6。 同样是RouteFuture按 body 的size_hint加的。mini 的响应里没有这个头;真正上线时它交给 hyper,hyper 编码响应时也会按size_hint选Content-Length还是 chunked(见 body 的解码与编码),用oneshot直接调才看得到这层差别。GET /nope:两边都是 404,axum 的默认 fallback 也是一个空 body 的 404。
小结
Handler<T, S>的宏 impl 只有十几行:拆请求、前面的参数跑FromRequestParts、拼回去给最后一个跑FromRequest、失败就返回 rejection、调用函数、into_response。mini 和 axum 在这一层几乎逐行相同。T = (M, T1, ..., Tn)里每一项都有用:参数类型和M不放进T,就是 E0207;不用M、按最后一个参数是哪种提取器写两个 impl,就是 E0119。- 提取器的结果要先
let出来再await用户函数,否则Result里不要求Send的 rejection 会跨过await,future 不是Send。 Router在route里把 handler 擦成统一的盒子,Service::call进门就把任意 body 换成统一的Body;axum 从 0.7.0(#1751)起也是这样。- mini 缺的主要是路由:路径树、路径参数、按方法分层(405、
HEAD)、nest、中间件的作用范围,以及状态的延迟注入、body 上限和细分的拒绝类型。