Rust in Everything
基于 axum 0.8.9

从零写一个 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。不同的地方只有三处:

miniaxum 0.8.9
future 类型call 直接返回 BoxFutureHandler 有关联类型 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],编译不过时报错更短,见常见坑
📝NOTE
没有蠢问题

问: 每个提取器都写 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 总是再装一次。

📝NOTE
冷知识:axum 0.7 之前,每个类型都带着 body 类型参数 B

0.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 在这里会回什么,是下面的问题。
❗IMPORTANT
动脑筋:同一个 GET 路由,HEAD 和 DELETE 各得到什么?

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},冲突在启动时 panicmatchit 与路径语法、Router 的内部结构
路径参数没有,也就没有 Path 提取器匹配到的参数写进请求的 extensions,Path<T> 用 serde 反序列化Path、Query 与请求元数据
状态与 with_stateRouter::new(state) 一开始就要状态,所有 handler 共用一个 SRouter<S> 表示「还欠着 S」,with_state 把它变成 Router<S2>;FromRef 从总状态里取子状态;只有 Router<()> 是 ServiceState 与 FromRef、常见坑
MethodRouter 与 405方法是端点的一部分,方法不对就 404;HEAD 不会落到 GET每个路径一个 MethodRouter,九个方法槽,HEAD 先找 head 再找 get;没有匹配的方法回 405 并带 Allow;重复注册在启动时 panicMethodRouter
nest、merge、fallback都没有,找不到就固定 404nest 拆开子路由、加前缀逐条注册,merge 合并两棵树,fallback 可以逐层设置nest、merge 与 fallback
中间件只能把整个 Router 套进一个 tower LayerRouter::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_connectionaxum::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 上限和细分的拒绝类型。

💬 关联讨论

发起讨论
加载中...