錯誤處理

Rust 應用中的常規錯誤處理方式

Rust 的錯誤處理不同於 Java 等語言,它沒有 try...catch 這種語法,正常的做法是在應用程式層面定義全局的錯誤處理類型:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("io: `{0}`")]
    Io(#[from] io::Error),
    #[error("utf8: `{0}`")]
    FromUtf8(#[from] FromUtf8Error),
    #[error("diesel: `{0}`")]
    Diesel(#[from] diesel::result::Error),
    ...
}

pub type AppResult<T> = Result<T, AppError>;

這裡使用了 thiserror 這個函式庫,它可以方便地定義你自己的自訂錯誤類型,簡化程式碼。為了簡化書寫,順便定義一個 AppResult

thiserror 與 anyhow

在 Rust 錯誤處理的生態中,兩個常用的函式庫是 thiserroranyhow

  • thiserror: 適用於函式庫開發者,用於定義清晰的錯誤類型。它透過派生巨集幫助您為自訂錯誤類型實現 std::error::Error trait,同時允許您定義錯誤的表示方式。當您構建一個函式庫或者需要為使用者提供清晰的錯誤類型時,thiserror 是更好的選擇。

  • anyhow: 適用於應用程式開發者,提供了一個通用的錯誤類型 anyhow::Error,能夠包含任何實現了 std::error::Error trait 的錯誤。它更側重於錯誤的傳播而非定義,尤其適合應用層程式碼,您可以快速將各種錯誤轉換為 anyhow::Error,減少編寫樣板程式碼的需要。

在某些場景中,您可能會同時使用這兩個函式庫:在函式庫中使用 thiserror 定義錯誤類型,在應用程式中使用 anyhow 處理和傳播這些錯誤。

Handler 中的錯誤處理

在 Salvo 中,Handler 也經常會遇到各式錯誤,比如:資料庫連接錯誤、檔案存取錯誤、網路連接錯誤等等。對於這類型的錯誤,可以採用上述的錯誤處理手法:

#[handler]
async fn home()-> AppResult<()> {

}

這裡的 home 就直接返回了一個 AppResult<()>。但是,這個錯誤該如何顯示呢?我們需要為 AppResult 這個自訂錯誤類型實現 Writer,在這個實現中我們可以決定如何顯示錯誤:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        res.render(Text::Plain("I'm a error, hahaha!"));
    }
}

Salvo 中的 Handler 可以返回 Result,只需要 Result 中的 OkErr 的類型都實現 Writer trait。

使用 anyhow 進行錯誤處理

考慮到 anyhow 的使用比較廣泛,Salvo 提供了對 anyhow::Error 的內建支援。在開啟 anyhow 功能後,anyhow::Error 會實現 Writer trait,它會被映射為 InternalServerError

#[cfg(feature = "anyhow")]
#[async_trait]
impl Writer for ::anyhow::Error {
    async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
        res.render(StatusError::internal_server_error());
    }
}

要使用 anyhow 功能,需要在 Cargo.toml 中啟用 Salvo 的 anyhow feature:

[dependencies]
salvo = { version = "*", features = ["anyhow"] }
anyhow = "1.0"

這樣,您的處理函式就可以直接返回 anyhow::Result<T>

#[handler]
async fn home() -> anyhow::Result<impl Writer> {
    let data = fetch_data().context("Failed to fetch data")?;
    Ok(Text::Plain(data))
}

Error 中往往包含一些敏感資訊,一般情況下,並不想被普通使用者看到,那樣也太不安全了,一點點隱私也沒有了。但是,如果你是開發人員或者網站管理員,或許想法就不一樣了,你希望錯誤能把外衣脫得光光的,讓你看到最真實的錯誤資訊。

可以看到,write 的方法中,我們其實是可以拿到 RequestDepot 的引用的,這就可以很方便地實現上面的操作了:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        let user = depot.obtain::<User>();
        if user.is_admin {
            res.render(Text::Plain(e.to_string()));
        } else {
            res.render(Text::Plain("I'm a error, hahaha!"));
        }
    }
}

錯誤頁面的顯示

Salvo 中自帶的錯誤頁面在絕大部分情況下是滿足需求的,它可以根據請求的資料類型顯示 Html、Json 或者 Xml 頁面。然而,某些情況下,我們依然期望自訂錯誤頁面的顯示。

這個可以透過自訂 Catcher 實現。詳細的介紹可以查看 Catcher 部分的講解。