Gestione degli Errori

Approcci Comuni alla Gestione degli Errori nelle Applicazioni Rust

La gestione degli errori in Rust differisce da linguaggi come Java; manca di costrutti come try...catch. L'approccio tipico consiste nel definire un tipo di errore globale a livello dell'applicazione:

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

Qui viene utilizzata la libreria thiserror, che facilita la definizione di tipi di errore personalizzati e semplifica il codice. Per brevità, viene anche definito un alias di tipo AppResult.

thiserror vs. anyhow

Nell'ecosistema della gestione degli errori in Rust, due librerie comunemente utilizzate sono thiserror e anyhow:

  • thiserror: Adatta agli sviluppatori di librerie per definire tipi di errore chiari. Utilizza macro derive per aiutare a implementare il tratto std::error::Error per tipi di errore personalizzati, consentendo allo stesso tempo di definire le rappresentazioni degli errori. Quando si costruisce una libreria o si forniscono tipi di errore chiari agli utenti, thiserror è la scelta migliore.

  • anyhow: Rivolta agli sviluppatori di applicazioni, fornisce un tipo di errore generico anyhow::Error che può incapsulare qualsiasi errore che implementi il tratto std::error::Error. Si concentra più sulla propagazione che sulla definizione degli errori, rendendola particolarmente adatta per il codice a livello applicativo. È possibile convertire rapidamente vari errori in anyhow::Error, riducendo la necessità di codice boilerplate.

In alcuni scenari, potresti utilizzare entrambe le librerie: definire i tipi di errore con thiserror nelle librerie e gestire e propagare questi errori con anyhow nelle applicazioni.

Gestione degli Errori negli Handler

In Salvo, gli Handler incontrano spesso vari errori, come errori di connessione al database, errori di accesso ai file, errori di connessione di rete, ecc. Per questo tipo di errori, è possibile applicare l'approccio di gestione degli errori sopra menzionato:

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

}

Qui, home restituisce direttamente un AppResult<()>. Ma come dovrebbe essere visualizzato questo errore? Dobbiamo implementare il tratto Writer per il tipo di errore personalizzato AppResult, dove possiamo decidere come visualizzare l'errore:

#[async_trait]
impl Writer for AppError {
    async fn write(mut self, _req: &mut Request, depot: &mut Depot, res: &mut Response) {
        res.render(Text::Plain("Sono un errore, ahahah!"));
    }
}

In Salvo, un Handler può restituire un Result, a condizione che sia il tipo Ok che il tipo Err all'interno del Result implementino il tratto Writer.

Gestione degli Errori con anyhow

Dato l'uso diffuso di anyhow, Salvo fornisce supporto integrato per anyhow::Error. Quando la feature anyhow è abilitata, anyhow::Error implementa il tratto Writer ed è mappato su 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());
    }
}

Per utilizzare la feature anyhow, abilita la feature anyhow di Salvo in Cargo.toml:

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

Ciò consente alle tue funzioni handler di restituire direttamente anyhow::Result<T>:

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

Gli errori spesso contengono informazioni sensibili, che generalmente non dovrebbero essere visibili agli utenti normali per motivi di sicurezza e privacy. Tuttavia, se sei uno sviluppatore o un amministratore del sito, potresti preferire che gli errori siano completamente esposti, rivelando le informazioni di errore più accurate.

Come mostrato, nel metodo write, possiamo accedere ai riferimenti a Request e Depot, rendendo conveniente implementare l'approccio sopra descritto:

#[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("Sono un errore, ahahah!"));
        }
    }
}

Visualizzazione delle Pagine di Errore

Le pagine di errore integrate in Salvo soddisfano i requisiti nella maggior parte dei casi, visualizzando pagine Html, Json o Xml in base al tipo di dati della richiesta. Tuttavia, ci sono situazioni in cui si desidera ancora una visualizzazione personalizzata della pagina di errore.

Ciò può essere ottenuto implementando un Catcher personalizzato. Per istruzioni dettagliate, fare riferimento alla sezione Catcher.