为什么一定要定义自己的 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> 声明返回值这样代码还清晰些,只是需要多敲几下,有时候我感觉还是不懒一点比较好。