为什么一定要定义自己的 Error
先把错误放在前面
error[E0277]: the trait bound `anyhow::Error: Responder<'_, '_>` is not satisfied
--> src/main.rs:41:15
|
40 | #[get("/")]
| ----------- in this procedural macro expansion
41 | fn index() -> ApiResult<&'static str> {
| ^^^^^^^^^ the trait `Responder<'_, '_>` is not implemented for `anyhow::Error`
|
= help: the following other types implement trait `Responder<'r, 'o>`:
<rocket_ws::websocket::Channel<'o> as Responder<'r, 'o>>
<rocket_ws::websocket::MessageStream<'o, S> as Responder<'r, 'o>>
<error::Error as Responder<'r, 'static>>
<rocket::Either<T, E> as Responder<'r, 'o>>
<Box<str> as Responder<'r, 'static>>
<Box<[u8]> as Responder<'r, 'static>>
<Box<T> as Responder<'r, 'o>>
<Cow<'o, R> as Responder<'r, 'o>>
and 42 others
= note: required for `Result<rocket::serde::json::Json<Res<&str>>, anyhow::Error>` to implement `Responder<'_, '_>`
note: required by a bound in `route::handler::<impl Outcome<rocket::Response<'o>, Status, (rocket::Data<'o>, Status)>>::from`
--> /Users/jeremy/.cargo/registry/src/index.crates.io-6f17d22bba15001f/rocket-0.5.0/src/route/handler.rs:188:20
|
188 | pub fn from<R: Responder<'r, 'o>>(req: &'r Request<'_>, responder: R) -> Outcome<'r> {
| ^^^^^^^^^^^^^^^^^ required by this bound in `route::handler::<impl Outcome<Response<'o>, Status, (Data<'o>, Status)>>::from`
= note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info)
至于这个 ApiResult 嘛,就是这样的,直接就是一个 anyhow::Result 的别名。
use rocket::serde::json::Json;
use crate::entity::Res;
pub type ApiResult<T> = anyhow::Result<Json<Res<T>>>;
先说结论:这么做是行不通的,这是不可行的做法。
从一开始就迷迷糊糊地从 Python 改写出了几个项目后,我总是在想 rust 这边的 Error 有没有类似 Python 那边那样方便的业务 Error + Code 写法,就总在看这个业务系统的 Error 如何设计。
跟第三方库的 Error 的设计不一样,业务系统里面的错误总感觉是要复杂些的,不然就像第三方库那样直接声明一个或者几个 XXXError 枚举就可以了。
上面最开始这个报错就是提示了一个结论:anyhow::Error 不能直接用于最外层返回,因为没法为它实现 Responder,为啥,就是不能,Rust 的限制。
参考文档:Coherence
Responder 是 Rocket 提供的,anyhow::Error 是 anyhow 提供的,那还怎么玩呢,没法玩了呀。
所以,我之前那种迷迷糊糊走出来的路,竟然一条合理的路。
反而现在想的什么用 anyhow::Result 直接做返回这种想法直接就被干趴下了。
好了,结论就是:还是要一定要定义自己的 Error,用 thiserror 来实现。
接口返回那就还是用自己的 Error,业务逻辑层呢,有两个选择,一个是用 Result 加自己的 Error,这个也是之前一直迷迷糊糊得到的结果,有个不好的地方就是,要主动声明外部库的 Error 到自己这个 Error 的转换,也就是在自己的 Error 里面加个枚举声明一下就行了,其实也还好。
第二个选择是,用 anyhow::Result 做业务层的返回,好处是不用去声明了,不过,有一个比较大的不好的地方,就是返回 Error 用 .into() 转换成了 anyhow::Error 之后,在最终 Responder 的地方去做一些自定义处理的逻辑的时候,不是很方便了,信息丢了,比如在想根据不同的 Error 在消息体字段 code 里面返回不同的 Code 值,就不大行了。
嗯,这样看起来,竟然还是原来的路走对了。
API 层返回用自己的 Error,业务层也用自己的 Error,每多一个 Error 提示说无法自动转换,就去 Error 里面加一个声明。
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
#[error("{0:?}")]
Feedback(Code),
#[error("{0:?}")]
OpenAIError(#[from] OpenAIError)
}
impl Error {
pub fn get_code(&self) -> Code {
match self {
Error::Feedback(code) => code.clone(),
Error::Anyhow(_) => Code::Error,
Error::OpenAIError(_) => Code::Error,
}
}
}
impl<'r> Responder<'r, 'static> for Error {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
let res: Res<String> = Res::err(&self);
let string = serde_json::to_string(&res).map_err(|e| {
error_!("JSON failed to serialize: {:?}", e);
Status::InternalServerError
})?;
capture_anyhow(&format_err!("{:?}", self));
tracing::error!("{:?}", self);
content::RawJson(string).respond_to(req)
}
}
impl OpenApiResponderInner for Error {
fn responses(gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
let mut responses = <Responses>::default();
responses.responses.insert("400".to_string(), RefOr::Object(Response {
description: "\
# [400 Bad Request](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400)\n\
The request given is wrongly formatted or data asked could not be fulfilled. \
"
.to_string(),
..Default::default()
}),
);
Ok(responses)
}
}
那,差不多就类似这样。
哦对了,如果用这种方案,业务层里面的 Result 都得带上 Error 声明,你可以选择再定义一个自己的 Result 别名,也可以不定义,我倾向于不定义,直接用 Result<T, Error> 声明返回值这样代码还清晰些,只是需要多敲几下,有时候我感觉还是不懒一点比较好。