Table of contents
Open Table of contents
TL;DR
axum 的 handler 是普通 async fn,参数类型决定如何从 HTTP 请求中提取数据。FromRequestParts 提取头部/路径/query(不消耗 body),FromRequest 提取 body(只能有一个,必须放最后)。所有验证在编译期完成——类型不匹配编译器直接拒绝,不等到运行时 500。这背后是 all_the_tuples! 宏为 0~16 个参数生成的 17 个 Handler trait blanket impl。
Handler trait:async fn 如何变成请求处理器
trait 定义
pub trait Handler<T, S>: Clone + Send + Sized + 'static {
type Future: Future<Output = Response> + Send + 'static;
fn call(self, req: Request, state: S) -> Self::Future;
}
类型参数 T 是一个标记类型(marker type),用于解决 Rust 的 trait coherence 限制——没有它,编译器无法区分 Fn(A) -> X 和 Fn(A, B) -> Y 的不同 blanket impl。
编译器看到的
当你写:
async fn get_user(
Path(id): Path<u32>,
State(db): State<PgPool>,
Json(body): Json<UpdateUser>,
) -> impl IntoResponse {
// ...
}
编译器做了什么:
all_the_tuples!宏生成了impl Handler<(M, T1, T2, T3), S>用于 3 参数函数- 对于前 N-1 个参数(
Path<u32>,State<PgPool>),要求实现FromRequestParts<S> - 对于最后一个参数(
Json<UpdateUser>),要求实现FromRequest<S, M> - 返回类型要求实现
IntoResponse - 函数本身要求
Clone + Send + 'static
生成的代码(概念上):
// 编译器为这个 handler 生成的调用路径
fn call(self, req: Request, state: S) -> Self::Future {
async move {
let (mut parts, body) = req.into_parts();
// 逐个提取 FromRequestParts 参数
let t1 = match Path::<u32>::from_request_parts(&mut parts, &state).await {
Ok(v) => v,
Err(rejection) => return rejection.into_response(),
};
let t2 = match State::<PgPool>::from_request_parts(&mut parts, &state).await {
Ok(v) => v,
Err(rejection) => return rejection.into_response(),
};
// 重新组装请求,提取 FromRequest 参数(消耗 body)
let req = Request::from_parts(parts, body);
let t3 = match Json::<UpdateUser>::from_request(req, &state).await {
Ok(v) => v,
Err(rejection) => return rejection.into_response(),
};
// 调用用户函数
self(t1, t2, t3).await.into_response()
}
}
全部在编译期单态化——零运行时分发开销。
双 trait 架构:FromRequestParts vs FromRequest
// 不消耗 body,可以在任意位置
pub trait FromRequestParts<S>: Sized {
type Rejection: IntoResponse;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection>;
}
// 消耗 body,只能作为最后一个参数
pub trait FromRequest<S, M = private::ViaRequest>: Sized {
type Rejection: IntoResponse;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection>;
}
FromRequest 的第二个类型参数 M 是 marker,用于实现一个 blanket impl:
// 所有 FromRequestParts 自动也是 FromRequest(通过 ViaParts marker)
impl<S, T> FromRequest<S, private::ViaParts> for T
where T: FromRequestParts<S>
这让 FromRequestParts 的类型也能出现在最后一个参数位置。
为什么 body 只能消耗一次
HTTP 请求的 body 是一个异步流(stream),读完就没了。FromRequest 拿到完整的 Request(包含 body),提取后 body 被消耗。如果允许两个 FromRequest 参数,第二个拿到的是空 body。
axum 用类型系统在编译期强制这个约束:前 N-1 个参数必须是 FromRequestParts(只能访问 headers/URI/method,不碰 body),最后一个可以是 FromRequest。
内置 Extractor 全表
FromRequestParts(不消耗 body,可以在任意位置)
| Extractor | 提取内容 | 示例 |
|---|---|---|
Path<T> | URL 路径参数 | Path(id): Path<u32> |
Query<T> | 查询字符串 ?key=val | Query(params): Query<SearchParams> |
HeaderMap | 所有请求头 | headers: HeaderMap |
Method | HTTP 方法 | method: Method |
Uri | 完整 URI | uri: Uri |
Version | HTTP 版本 | version: Version |
State<T> | 应用状态 | State(db): State<PgPool> |
Extension<T> | 请求级扩展数据 | Extension(user): Extension<AuthUser> |
ConnectInfo<T> | 客户端连接信息 | ConnectInfo(addr): ConnectInfo<SocketAddr> |
MatchedPath | 匹配的路由模式 | path: MatchedPath → "/users/{id}" |
NestedPath | 嵌套路由前缀 | prefix: NestedPath |
OriginalUri | nest 剥离前的 URI | uri: OriginalUri |
RawQuery | 未解析的查询字符串 | query: RawQuery |
WebSocketUpgrade | WebSocket 升级句柄 | ws: WebSocketUpgrade |
FromRequest(消耗 body,必须在最后)
| Extractor | 提取内容 | Content-Type |
|---|---|---|
Json<T> | JSON body → 反序列化 | application/json |
Form<T> | URL-encoded body | application/x-www-form-urlencoded |
String | body → UTF-8 字符串 | 任意 |
Bytes | body → 原始字节 | 任意 |
Multipart | 多部分表单/文件上传 | multipart/form-data |
Request | 完整请求 | 任意 |
body 大小限制
body 提取器默认拒绝超过 2MB 的请求体:
use axum::extract::DefaultBodyLimit;
let app = Router::new()
.route("/upload", post(upload))
.layer(DefaultBodyLimit::max(50 * 1024 * 1024)) // 50MB
// 或者完全禁用限制(危险)
// .layer(DefaultBodyLimit::disable())
自定义 Extractor
只读元数据(FromRequestParts)
struct ExtractUserAgent(HeaderValue);
impl<S> FromRequestParts<S> for ExtractUserAgent
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
parts.headers
.get(USER_AGENT)
.map(|v| ExtractUserAgent(v.clone()))
.ok_or((StatusCode::BAD_REQUEST, "Missing User-Agent"))
}
}
消耗 body(FromRequest)
struct ValidatedJson<T>(pub T);
impl<S, T> FromRequest<S> for ValidatedJson<T>
where
T: DeserializeOwned + Validate,
S: Send + Sync,
{
type Rejection = (StatusCode, String);
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Json(value) = Json::<T>::from_request(req, state)
.await
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
value.validate()
.map_err(|e| (StatusCode::UNPROCESSABLE_ENTITY, e.to_string()))?;
Ok(ValidatedJson(value))
}
}
也可以用派生宏:#[derive(FromRequest)] 和 #[derive(FromRequestParts)]。
Rejection:提取失败如何变成 HTTP 响应
每个 extractor 有一个关联的 Rejection 类型,实现了 IntoResponse。提取失败时 rejection 自动转换为 HTTP 响应。
显式处理 rejection
用 Result 包装 extractor 来拦截失败:
async fn handler(payload: Result<Json<Value>, JsonRejection>) -> impl IntoResponse {
match payload {
Ok(Json(value)) => (StatusCode::OK, Json(value)).into_response(),
Err(JsonRejection::MissingJsonContentType(_)) => {
(StatusCode::UNSUPPORTED_MEDIA_TYPE, "Expected application/json").into_response()
}
Err(JsonRejection::JsonSyntaxError(e)) => {
(StatusCode::BAD_REQUEST, format!("Invalid JSON: {e}")).into_response()
}
Err(e) => (StatusCode::BAD_REQUEST, e.to_string()).into_response(),
}
}
用 Option 包装使提取可选(失败返回 None)。
调试 rejection:RUST_LOG=axum::rejection=trace。
响应系统:IntoResponse
pub trait IntoResponse {
fn into_response(self) -> Response<Body>;
}
内置实现
| 类型 | 状态码 | Content-Type |
|---|---|---|
() | 200 | 空 body |
&'static str | 200 | text/plain; charset=utf-8 |
String | 200 | text/plain; charset=utf-8 |
Vec<u8> | 200 | application/octet-stream |
Json<T> | 200 | application/json |
Html<T> | 200 | text/html |
StatusCode | 给定值 | 空 body |
Redirect | 3xx | Location header |
NoContent | 204 | 空 body(0.8 新增) |
Tuple 组合
axum 为元组实现了 IntoResponse(最多 17 元素):
// 状态码 + body
async fn created() -> (StatusCode, String) {
(StatusCode::CREATED, "created".to_string())
}
// 状态码 + 自定义头 + body
async fn with_headers() -> (StatusCode, [(HeaderName, &'static str); 1], Json<Value>) {
(
StatusCode::OK,
[(CONTENT_TYPE, "application/json")],
Json(json!({"ok": true})),
)
}
规则:第一个元素可以是 StatusCode;中间元素实现 IntoResponseParts(headers、extensions);最后一个元素实现 IntoResponse(body)。
自定义响应类型
struct ApiResponse<T: Serialize> {
status: StatusCode,
data: T,
}
impl<T: Serialize> IntoResponse for ApiResponse<T> {
fn into_response(self) -> Response<Body> {
(self.status, Json(json!({ "data": self.data }))).into_response()
}
}
Result 作为响应
Result<T, E> 在 T: IntoResponse 且 E: IntoResponse 时自动实现 IntoResponse:
async fn get_user(Path(id): Path<i64>) -> Result<Json<User>, AppError> {
let user = find_user(id).await.map_err(AppError::Database)?;
Ok(Json(user))
}