Fehlerbehandlung

Gängige Ansätze zur Fehlerbehandlung in Rust-Anwendungen

Die Fehlerbehandlung in Rust unterscheidet sich von Sprachen wie Java; es gibt keine Konstrukte wie try...catch. Der typische Ansatz besteht darin, einen globalen Fehlerbehandlungstyp auf Anwendungsebene zu definieren:

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>;

Hier wird die Bibliothek thiserror verwendet, die das Definieren benutzerdefinierter Fehlertypen erleichtert und den Code vereinfacht. Der Übersichtlichkeit halber wird auch ein Typalias AppResult definiert.

thiserror vs. anyhow

Im Rust-Fehlerbehandlungs-Ökosystem werden zwei häufig verwendete Bibliotheken eingesetzt: thiserror und anyhow:

  • thiserror: Geeignet für Bibliotheksentwickler, um klare Fehlertypen zu definieren. Es verwendet Derive-Makros, um die Implementierung des std::error::Error-Traits für benutzerdefinierte Fehlertypen zu unterstützen, während gleichzeitig die Definition von Fehlerdarstellungen ermöglicht wird. Beim Erstellen einer Bibliothek oder beim Bereitstellen klarer Fehlertypen für Benutzer ist thiserror die bessere Wahl.

  • anyhow: Richtet sich an Anwendungsentwickler und bietet einen generischen Fehlertyp anyhow::Error, der jeden Fehler kapseln kann, der den std::error::Error-Trait implementiert. Der Fokus liegt mehr auf der Fehlerweitergabe als auf der Definition, was es besonders für Code auf Anwendungsebene geeignet macht. Verschiedene Fehler können schnell in anyhow::Error umgewandelt werden, wodurch der Bedarf an Boilerplate-Code reduziert wird.

In einigen Szenarien können beide Bibliotheken verwendet werden: Definition von Fehlertypen mit thiserror in Bibliotheken und Behandlung sowie Weitergabe dieser Fehler mit anyhow in Anwendungen.

Fehlerbehandlung in Handlern

In Salvo stoßen Handler oft auf verschiedene Fehler, wie Datenbankverbindungsfehler, Dateizugriffsfehler, Netzwerkverbindungsfehler usw. Für diese Arten von Fehlern kann der oben beschriebene Fehlerbehandlungsansatz angewendet werden:

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

}

Hier gibt home direkt ein AppResult<()> zurück. Aber wie sollte dieser Fehler angezeigt werden? Wir müssen den Writer-Trait für den benutzerdefinierten Fehlertyp AppResult implementieren, wo wir entscheiden können, wie der Fehler angezeigt wird:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        res.render(Text::Plain("Ich bin ein Fehler, hahaha!"));
    }
}

In Salvo kann ein Handler ein Result zurückgeben, vorausgesetzt, dass sowohl der Ok- als auch der Err-Typ im Result den Writer-Trait implementieren.

Fehlerbehandlung mit anyhow

Aufgrund der weit verbreiteten Verwendung von anyhow bietet Salvo integrierte Unterstützung für anyhow::Error. Wenn das anyhow-Feature aktiviert ist, implementiert anyhow::Error den Writer-Trait und wird auf InternalServerError abgebildet:

#[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());
    }
}

Um das anyhow-Feature zu verwenden, aktivieren Sie das anyhow-Feature von Salvo in Cargo.toml:

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

Dies ermöglicht es Ihren Handler-Funktionen, direkt anyhow::Result<T> zurückzugeben:

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

Fehler enthalten oft sensible Informationen, die aus Sicherheits- und Datenschutzgründen normalerweise nicht für reguläre Benutzer sichtbar sein sollten. Wenn Sie jedoch ein Entwickler oder Site-Administrator sind, möchten Sie möglicherweise, dass Fehler vollständig offengelegt werden, um die genauesten Fehlerinformationen anzuzeigen.

Wie gezeigt, können wir in der write-Methode auf Referenzen von Request und Depot zugreifen, was die Implementierung des obigen Ansatzes vereinfacht:

#[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("Ich bin ein Fehler, hahaha!"));
        }
    }
}

Anzeigen von Fehlerseiten

Die integrierten Fehlerseiten von Salvo erfüllen in den meisten Fällen die Anforderungen und zeigen Html-, Json- oder Xml-Seiten basierend auf dem Datentyp der Anfrage an. Es gibt jedoch Situationen, in denen benutzerdefinierte Fehlerseitenanzeigen gewünscht sind.

Dies kann durch die Implementierung eines benutzerdefinierten Catcher erreicht werden. Detaillierte Anweisungen finden Sie im Abschnitt Catcher.