Gestion des erreurs

Approches courantes de gestion des erreurs dans les applications Rust

La gestion des erreurs en Rust diffère des langages comme Java ; elle ne dispose pas de constructions telles que try...catch. L'approche typique consiste à définir un type d'erreur global au niveau de l'application :

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

Ici, la bibliothèque thiserror est utilisée, ce qui facilite la définition de types d'erreur personnalisés et simplifie le code. Par souci de concision, un alias de type AppResult est également défini.

thiserror vs. anyhow

Dans l'écosystème de gestion des erreurs de Rust, deux bibliothèques couramment utilisées sont thiserror et anyhow :

  • thiserror : Convient aux développeurs de bibliothèques pour définir des types d'erreur clairs. Elle utilise des macros derive pour aider à implémenter le trait std::error::Error pour les types d'erreur personnalisés tout en permettant de définir les représentations d'erreur. Lors de la construction d'une bibliothèque ou de la fourniture de types d'erreur clairs aux utilisateurs, thiserror est le meilleur choix.

  • anyhow : Destinée aux développeurs d'applications, elle fournit un type d'erreur générique anyhow::Error qui peut encapsuler toute erreur implémentant le trait std::error::Error. Elle se concentre davantage sur la propagation des erreurs que sur leur définition, ce qui la rend particulièrement adaptée au code de la couche application. Vous pouvez rapidement convertir diverses erreurs en anyhow::Error, réduisant ainsi le besoin de code passe-partout.

Dans certains scénarios, vous pourriez utiliser les deux bibliothèques : définir les types d'erreur avec thiserror dans les bibliothèques et gérer et propager ces erreurs avec anyhow dans les applications.

Gestion des erreurs dans les gestionnaires

Dans Salvo, les Handler rencontrent souvent diverses erreurs, telles que des erreurs de connexion à la base de données, des erreurs d'accès aux fichiers, des erreurs de connexion réseau, etc. Pour ces types d'erreurs, l'approche de gestion des erreurs mentionnée précédemment peut être appliquée :

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

}

Ici, home retourne directement un AppResult<()>. Mais comment cette erreur doit-elle être affichée ? Nous devons implémenter le trait Writer pour le type d'erreur personnalisé AppResult, où nous pouvons décider comment afficher l'erreur :

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

Dans Salvo, un Handler peut retourner un Result, à condition que les types Ok et Err dans le Result implémentent tous deux le trait Writer.

Gestion des erreurs avec anyhow

Étant donné l'utilisation répandue d'anyhow, Salvo fournit une prise en charge intégrée pour anyhow::Error. Lorsque la fonctionnalité anyhow est activée, anyhow::Error implémente le trait Writer et est mappé sur 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());
    }
}

Pour utiliser la fonctionnalité anyhow, activez la fonctionnalité anyhow de Salvo dans Cargo.toml :

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

Cela permet à vos fonctions gestionnaires de retourner directement anyhow::Result<T> :

#[handler]
async fn home() -> anyhow::Result<impl Writer> {
    let data = fetch_data().context("Échec de la récupération des données")?;
    Ok(Text::Plain(data))
}

Les erreurs contiennent souvent des informations sensibles, qui ne devraient généralement pas être visibles par les utilisateurs ordinaires pour des raisons de sécurité et de confidentialité. Cependant, si vous êtes un développeur ou un administrateur de site, vous pourriez préférer que les erreurs soient entièrement exposées, révélant ainsi les informations d'erreur les plus précises.

Comme illustré, dans la méthode write, nous pouvons accéder aux références de Request et Depot, ce qui facilite la mise en œuvre de l'approche ci-dessus :

#[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("Je suis une erreur, hahaha !"));
        }
    }
}

Affichage des pages d'erreur

Les pages d'erreur intégrées de Salvo répondent aux exigences dans la plupart des cas, affichant des pages Html, Json ou Xml en fonction du type de données de la requête. Cependant, il existe des situations où des affichages de pages d'erreur personnalisés sont toujours souhaités.

Cela peut être réalisé en implémentant un Catcher personnalisé. Pour des instructions détaillées, reportez-vous à la section Catcher.