如何用 axum-macros 的 derive(FromRequest) 定义自定义提取器并控制 rejection
【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum
在 axum 应用中,内置提取器(如axum::Json)出错时会返回固定的 rejection 类型,响应格式不受你控制。当你需要统一自己的错误格式(例如统一 JSON 字段、统一状态码来源)时,可以用 axum-macros 提供的derive(FromRequest)宏,包一层新的提取器,并把 rejection 换成自定义类型。本文以仓库示例 examples/customize-extractor-error/src/derive_from_request.rs 为主路径,演示“包装axum::Json+ 自定义 rejection”的完整做法,并给出运行验证方式和宏的已知限制。
宏本身由 axum-macros 实现(见 axum-macros/src/lib.rs、axum-macros/src/from_request/mod.rs),axum 在开启macrosfeature 后通过axum::extract::FromRequest再导出它(见 axum/src/extract/mod.rs)。
准备:依赖配置
按示例项目 examples/customize-extractor-error/Cargo.toml 的配置,关键依赖是带macros特性的axum,以及示例用到的serde、serde_json、tokio:
[dependencies] axum = { path = "../../axum", features = ["macros"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1.20", features = ["full"] }从 crates 使用axum时,只需保证 features 中包含macros。示例还引入了axum-extra(with-rejectionfeature)和thiserror,那是 README 中另外两种自定义 rejection 方式的依赖,不属于本宏主路径的必需项。
主路径:包装axum::Json并替换 rejection
目标是创建一个内部使用axum::Json、但 rejection 换成自定义类型ApiError的提取器。完整代码来自 examples/customize-extractor-error/src/derive_from_request.rs:
use axum::{ extract::rejection::JsonRejection, extract::FromRequest, http::StatusCode, response::IntoResponse, }; use serde::Serialize; use serde_json::{json, Value}; pub async fn handler(Json(value): Json<Value>) -> impl IntoResponse { Json(dbg!(value)) } // create an extractor that internally uses `axum::Json` but has a custom rejection #[derive(FromRequest)] #[from_request(via(axum::Json), rejection(ApiError))] pub struct Json<T>(T); // We implement `IntoResponse` for our extractor so it can be used as a response impl<T: Serialize> IntoResponse for Json<T> { fn into_response(self) -> axum::response::Response { let Self(value) = self; axum::Json(value).into_response() } } // We create our own rejection type #[derive(Debug)] pub struct ApiError { status: StatusCode, message: String, } // We implement `From<JsonRejection> for ApiError` impl From<JsonRejection> for ApiError { fn from(rejection: JsonRejection) -> Self { Self { status: rejection.status(), message: rejection.body_text(), } } } // We implement `IntoResponse` so `ApiError` can be used as a response impl IntoResponse for ApiError { fn into_response(self) -> axum::response::Response { let payload = json!({ "message": self.message, "origin": "derive_from_request" }); (self.status, axum::Json(payload)).into_response() } }各部分的作用与条件:
#[derive(FromRequest)]+ 容器属性#[from_request(via(axum::Json), rejection(ApiError))]:让派生实现走“整体提取”模式,即整个Json<T>的值一次性通过axum::Json<T>提取(而不是逐字段提取)。rejection(ApiError)指定了自定义 rejection 类型,因此必须提供From<JsonRejection> for ApiError。宏生成的实现会对提取错误调用From::from完成转换。- rejection 类型必须实现
IntoResponse,否则无法作为响应返回,这是宏文档中明确的要求。 - 示例中
Json<T>自身也实现了IntoResponse,使同一个类型既能当 handler 参数、又能当返回值;这是示例的设计选择,如果你的自定义提取器只作参数,这一步可以省略。 dbg!是标准库宏,打印并原样返回该值;示例用它让 handler 把解析出的值回显。
这里Json<T>(T)是单字段元组结构体加一个泛型参数,恰好符合宏对泛型的限制(见下文“已知限制”)。
运行与验证
示例入口 examples/customize-extractor-error/src/main.rs 注册了三条 POST 路由,其中/derive-from-request对应上面的 handler,服务绑定在127.0.0.1:3000:
let app = Router::new() .route("/with-rejection", post(with_rejection::handler)) .route("/custom-extractor", post(custom_extractor::handler)) .route("/derive-from-request", post(derive_from_request::handler)); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); axum::serve(listener, app).await;启动命令(见 examples/customize-extractor-error/README.md):
cargo run -p example-customize-extractor-error验证方式分两步:
- 编译即第一道验证:
From<JsonRejection> for ApiError缺失或IntoResponse未实现时,代码无法通过编译;宏对属性写法错误(如缺via)也会直接报编译错误,不必等到运行期。 - 请求验证:服务启动后向
POST http://127.0.0.1:3000/derive-from-request发送一个无法解析的 body(例如不带 JSON 内容的空 body),提取失败会走ApiError::from(JsonRejection)路径。按示例代码,此时响应是一个 JSON body,包含message和"origin": "derive_from_request"两个字段,状态码取自rejection.status();发送合法 JSON 时 handler 会把解析出的值回显。例如:
curl -X POST http://127.0.0.1:3000/derive-from-request以上响应结构是示例代码自身定义的,不是 axum 的固定输出,改动into_response实现后结构会随之变化。
rejection 的其他两种控制方式
除了主路径的“容器via+ 自定义 rejection”,宏文档(axum-macros/src/lib.rs 中derive(FromRequest)一节)还覆盖两种场景。
逐字段提取时的自定义 rejection
默认逐字段模式下,rejection 默认是axum::response::Response。用#[from_request(rejection(YourType))]换成自己的类型后,需要为每个字段提取器的 rejection 提供From转换(文档示例中的字段 rejection 是ExtensionRejection和StringRejection):
#[derive(FromRequest)] #[from_request(rejection(MyRejection))] struct MyExtractor { state: Extension<String>, body: String, } // This tells axum how to convert `Extension`'s rejections into `MyRejection` impl From<ExtensionRejection> for MyRejection { fn from(rejection: ExtensionRejection) -> Self { // ... } } // This tells axum how to convert `String`'s rejections into `MyRejection` impl From<StringRejection> for MyRejection { fn from(rejection: StringRejection) -> Self { // ... } } // All rejections must implement `IntoResponse` impl IntoResponse for MyRejection { fn into_response(self) -> Response { self.0 } }容器via但不指定 rejection
只写#[from_request(via(Extension))]不写rejection(...)时,rejection 就是“via 提取器”自身的 rejection(例如Extension对应ExtensionRejection),无需额外From实现。
state 属性与字段级via
- state 推断:状态类型一般自动推断;当无法推断多个候选时,宏会给出编译错误提示
can't infer state type, please add #[from_request(state = MyStateType)] attribute(见 axum-macros/src/from_request/mod.rs),此时显式写#[from_request(state(CustomState))]指定即可。字段类型是State<T>时会自动推断为T,无需显式指定。 - 字段级
#[from_request(via(...))]:让某个字段通过另一个提取器提取,字段本身不必实现FromRequest,例如#[from_request(via(Extension))] state: State,。via提取器必须是实现了FromRequest的泛型 newtype(单字段公开元组结构体);更复杂的 via 提取器需要手写FromRequest实现。 - 可选字段:字段级
via支持Option<_>和Result<_, _>字段,分别走OptionalFromRequestParts/Result路径提取。 - 枚举派生:
#[derive(FromRequest)]用在枚举上时必须有容器via,不支持泛型,且via不能写在变体或其字段上(均为编译错误)。
更多可编译的通过用例可以对照 axum-macros/tests/from_request/pass 目录(如named_via.rs、enum_via.rs、state_infer.rs等)。
已知限制
这些来自宏文档的 “Known limitations” 一节及实现中的编译错误信息,出现对应写法时会在编译期报错:
- 泛型只支持“恰好一个字段的元组结构体”:
struct MyJson<T>(T)可以,struct MyExtractor<T> { thing: Option<T> }不行;泛型结构体用命名字段、带where子句、生命周期泛型或 const 泛型都不支持。 - 不使用
via时泛型同样不允许(only supports generics when used with #[from_request(via)])。 - 容器级
via(...)与字段级via(...)不能同时使用,变体/字段级via与容器via同现也是编译错误。 - 逐字段提取模式下,只有最后一个字段能消费 request body,前面的字段只能实现
FromRequestParts(或经via走 parts 提取)。 - 仓库文档标注该宏还有一些已知限制,示例文件头部引用了 docs.rs 上的 “Known limitations” 说明,以 axum-macros 发布文档为准。
替代路径(同一示例中的另外两种方式)
examples/customize-extractor-error/README.md 说明该示例共探索三种自定义 rejection 的方式,另两种可作为对照选择,不属于本宏路径:
- with_rejection:用
axum_extra::extract::WithRejection把一个 rejection 转换成另一个,不需要派生。 - custom_extractor:手写
FromRequest实现。README 指出手写实现能拿到RequestParts和async/await,可以构造更复杂的 rejection(例如该示例里先提取MatchedPath再提取 body),代价是代码更复杂、每个自定义 rejection 都要写一个提取器。
如果derive(FromRequest)的 via 限制满足不了你的提取逻辑(比如需要在提取过程中做额外处理),就切换到手写实现这条路,而不是强行用宏属性硬凑。
【免费下载链接】axumHTTP routing and request-handling library for Rust that focuses on ergonomics and modularity项目地址: https://gitcode.com/GitHub_Trending/ax/axum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考