錯誤處理

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 trait,在此實現中可決定錯誤的顯示方式:

#[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 章節。