如何用 axum-macros 的 derive(FromRequest) 定义自定义提取器并控制 rejection 如何用 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.rsaxum 在开启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-extrawith-rejectionfeature和thiserror那是 README 中另外两种自定义 rejection 方式的依赖不属于本宏主路径的必需项。主路径包装axum::Json并替换 rejection目标是创建一个内部使用axum::Json、但 rejection 换成自定义类型ApiError的提取器。完整代码来自 examples/customize-extractor-error/src/derive_from_request.rsuse axum::{ extract::rejection::JsonRejection, extract::FromRequest, http::StatusCode, response::IntoResponse, }; use serde::Serialize; use serde_json::{json, Value}; pub async fn handler(Json(value): JsonValue) - 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 JsonT(T); // We implement IntoResponse for our extractor so it can be used as a response implT: Serialize IntoResponse for JsonT { 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 FromJsonRejection for ApiError impl FromJsonRejection 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))]让派生实现走“整体提取”模式即整个JsonT的值一次性通过axum::JsonT提取而不是逐字段提取。rejection(ApiError)指定了自定义 rejection 类型因此必须提供FromJsonRejection for ApiError。宏生成的实现会对提取错误调用From::from完成转换。rejection 类型必须实现IntoResponse否则无法作为响应返回这是宏文档中明确的要求。示例中JsonT自身也实现了IntoResponse使同一个类型既能当 handler 参数、又能当返回值这是示例的设计选择如果你的自定义提取器只作参数这一步可以省略。dbg!是标准库宏打印并原样返回该值示例用它让 handler 把解析出的值回显。这里JsonT(T)是单字段元组结构体加一个泛型参数恰好符合宏对泛型的限制见下文“已知限制”。运行与验证示例入口 examples/customize-extractor-error/src/main.rs 注册了三条 POST 路由其中/derive-from-request对应上面的 handler服务绑定在127.0.0.1:3000let 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.mdcargo run -p example-customize-extractor-error验证方式分两步编译即第一道验证FromJsonRejection 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: ExtensionString, body: String, } // This tells axum how to convert Extensions rejections into MyRejection impl FromExtensionRejection for MyRejection { fn from(rejection: ExtensionRejection) - Self { // ... } } // This tells axum how to convert Strings rejections into MyRejection impl FromStringRejection 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 属性与字段级viastate 推断状态类型一般自动推断当无法推断多个候选时宏会给出编译错误提示cant infer state type, please add #[from_request(state MyStateType)] attribute见 axum-macros/src/from_request/mod.rs此时显式写#[from_request(state(CustomState))]指定即可。字段类型是StateT时会自动推断为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 MyJsonT(T)可以struct MyExtractorT { thing: OptionT }不行泛型结构体用命名字段、带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),仅供参考