Gestionnaire

Aperçu rapide

Le gestionnaire est un concept central dans le framework Salvo, qui peut être simplement compris comme une unité de traitement des requêtes. Il a deux objectifs principaux :

  1. En tant que point de terminaison : Un objet implémentant Handler peut être placé dans le système de routage comme point de terminaison final pour traiter les requêtes. Lors de l'utilisation de la macro #[handler], une fonction peut être directement utilisée comme point de terminaison ; tandis que l'utilisation de la macro #[endpoint] permet non seulement de servir de point de terminaison, mais génère également automatiquement la documentation OpenAPI (cela sera détaillé dans la documentation ultérieure).

  2. En tant que middleware : Le même Handler peut également être utilisé comme middleware pour traiter les requêtes avant ou après qu'elles atteignent le point de terminaison final.

Le flux de traitement des requêtes de Salvo peut être vu comme un "pipeline" : une requête passe d'abord par une série de middlewares (traitement vertical) puis atteint le point de terminaison correspondant (traitement horizontal). Les middlewares et les points de terminaison sont tous deux des implémentations de Handler, ce qui garantit la cohérence et la flexibilité de l'ensemble du système.

Diagramme de flux du gestionnaire dans Salvo

flowchart TD
    Client[Client] -->|Requête| Request[Requête HTTP]
    Request --> M1

    subgraph "Implémentation du gestionnaire"
        M1[Middleware de journalisation] -->|Traitement vertical| M2[Middleware d'authentification] -->|Traitement vertical| M3[Autre middleware métier]
        M3 --> Router[Correspondance de route]

        Router -->|Traitement horizontal| E1["Point de terminaison 1<br>#[handler]"]
        Router -->|Traitement horizontal| E2["Point de terminaison 2<br>#[endpoint]"]
        Router -->|Traitement horizontal| E3["Point de terminaison 3<br>Implémente le trait Handler"]
    end

    E1 --> Response[Réponse HTTP]
    E2 --> Response
    E3 --> Response
    Response --> Client

    classDef middleware fill:#afd,stroke:#4a4,stroke-width:2px;
    classDef endpoint fill:#bbf,stroke:#55a,stroke-width:2px;

    class M1,M2,M3 middleware;
    class E1,E2,E3 endpoint;

Middleware et modèle en oignon

L'essence du modèle en oignon est qu'en plaçant ctrl.call_next() avant et après une logique spécifique, il met en œuvre un flux de traitement bidirectionnel pour les requêtes et les réponses, permettant à chaque middleware de participer au cycle complet requête-réponse.

Structure d'exemple complète de middleware

async fn example_middleware(req: &mut Request, depot: &mut Depot, resp: &mut Response, ctrl: &mut FlowCtrl) {
    // Prétraitement (phase de requête)
    // Placer ici la logique à exécuter lorsque la requête entre.

    // Appeler le gestionnaire suivant dans la chaîne.
    ctrl.call_next(req, depot, resp).await;

    // Post-traitement (phase de réponse)
    // Placer ici la logique à exécuter après le traitement de la requête.
}
flowchart TB
    Client[Client] -->|Requête| Request[Requête HTTP]

    subgraph "Flux de traitement du modèle en oignon"
        direction TB

        subgraph M1["Middleware 1"]
            M1A["Prétraitement<br>Journalisation : début de requête"] --> M1B["Post-traitement<br>Journalisation : fin de requête"]
        end

        subgraph M2["Middleware 2"]
            M2A["Prétraitement<br>Enregistrer l'heure de début"] --> M2B["Post-traitement<br>Calculer le temps de traitement"]
        end

        subgraph M3["Middleware 3"]
            M3A["Prétraitement<br>Valider les permissions utilisateur"] --> M3B["Post-traitement<br>"]
        end

        subgraph Core["Logique de traitement principale"]
            Handler["Gestionnaire de point de terminaison<br>#[handler]"]
        end

        Request --> M1A
        M1A --> M2A
        M2A --> M3A
        M3A --> Handler
        Handler --> M3B
        M3B --> M2B
        M2B --> M1B
        M1B --> Response
    end

    Response[Réponse HTTP] --> Client

    classDef middleware1 fill:#f9d,stroke:#333,stroke-width:2px;
    classDef middleware2 fill:#afd,stroke:#333,stroke-width:2px;
    classDef middleware3 fill:#bbf,stroke:#333,stroke-width:2px;
    classDef core fill:#fd7,stroke:#333,stroke-width:2px;

    class M1A,M1B middleware1;
    class M2A,M2B middleware2;
    class M3A,M3B middleware3;
    class Handler core;

Qu'est-ce qu'un gestionnaire

Un gestionnaire est l'objet concret responsable du traitement des objets Request. Handler est lui-même un trait contenant une méthode asynchrone handle :

#[async_trait]
pub trait Handler: Send + Sync + 'static {
    async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response);
}

La signature par défaut de la fonction handle comprend quatre paramètres, dans l'ordre : &mut Request, &mut Depot, &mut Response, &mut FlowCtrl. Depot est un stockage temporaire qui peut contenir des données liées à la requête en cours.

Selon son utilisation, il peut servir de middleware (hoop), qui peut effectuer un traitement avant ou après que la requête atteigne le Handler de traitement formel des requêtes, comme : vérification de connexion, compression de données, etc.

Le middleware est ajouté via la fonction hoop d'un Router. Le middleware ajouté affecte le Router actuel et tous ses Router descendants.

Un Handler peut également être utilisé comme un Handler qui participe à la correspondance de route et est finalement exécuté, appelé goal.

Handler en tant que middleware (hoop)

Lorsqu'un Handler agit en tant que middleware, il peut être ajouté aux trois types d'objets suivants qui prennent en charge le middleware :

  • Service : Toute requête passera par le middleware dans le Service.
  • Router : Ce n'est que lorsque la correspondance de route réussit que la requête passera par le middleware défini dans le Service et tout le middleware collecté le long du chemin de correspondance.
  • Catcher : Lorsqu'une erreur se produit et qu'aucune information d'erreur personnalisée n'a été écrite, la requête passera par le middleware dans le Catcher.
  • Handler : Le Handler lui-même prend en charge l'ajout d'encapsulations de middleware pour exécuter une logique pré- ou post-traitement.

Utilisation de la macro #[handler]

La macro #[handler] peut grandement simplifier l'écriture du code et améliorer la flexibilité du code.

Elle peut être appliquée à une fonction pour la faire implémenter Handler :

#[handler]
async fn hello() -> &'static str {
    "hello world!"
}

Cela équivaut à :

struct hello;

#[async_trait]
impl Handler for hello {
    async fn handle(&self, _req: &mut Request, _depot: &mut Depot, res: &mut Response, _ctrl: &mut FlowCtrl) {
        res.render(Text::Plain("hello world!"));
    }
}

Comme vous pouvez le voir, avec #[handler], le code devient beaucoup plus simple :

  • Pas besoin d'ajouter manuellement #[async_trait].
  • Les paramètres inutiles dans la fonction sont omis, et les paramètres requis peuvent être arrangés dans n'importe quel ordre.
  • Pour les objets implémentant les abstractions Writer ou Scribe, ils peuvent être directement retournés comme valeur de retour de la fonction. Ici, &'static str implémente Scribe, donc il peut être directement retourné.

#[handler] peut non seulement être appliqué à des fonctions, mais aussi au bloc impl d'une struct pour faire implémenter Handler par la struct. Dans ce cas, la fonction handle à l'intérieur du bloc impl est reconnue comme l'implémentation concrète de la méthode handle dans Handler :

struct Hello;

#[handler]
impl Hello {
    async fn handle(&self, res: &mut Response) {
        res.render(Text::Plain("hello world!"));
    }
}

Gestion des erreurs

Dans Salvo, un Handler peut retourner un Result, à condition que les types Ok et Err à l'intérieur du Result implémentent le trait Writer. Considérant l'utilisation répandue d'anyhow, lorsque la fonctionnalité anyhow est activée, anyhow::Error implémentera le trait Writer. anyhow::Error sera 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 les types d'erreur personnalisés, vous pouvez afficher différentes pages d'erreur selon les besoins.

use salvo::anyhow;
use salvo::prelude::*;

struct CustomError;
#[async_trait]
impl Writer for CustomError {
    async fn write(mut self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
        res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
        res.render("custom error");
    }
}

#[handler]
async fn handle_anyhow() -> Result<(), anyhow::Error> {
    Err(anyhow::anyhow!("anyhow error"))
}
#[handler]
async fn handle_custom() -> Result<(), CustomError> {
    Err(CustomError)
}

#[tokio::main]
async fn main() {
    let router = Router::new()
        .push(Router::new().path("anyhow").get(handle_anyhow))
        .push(Router::new().path("custom").get(handle_custom));
    let acceptor = TcpListener::new("127.0.0.1:8698").bind().await;
    Server::new(acceptor).serve(router).await;
}

Implémentation directe du trait Handler

use salvo_core::prelude::*;
use crate::salvo_core::http::Body;

pub struct MaxSizeHandler(u64);
#[async_trait]
impl Handler for MaxSizeHandler {
    async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
        if let Some(upper) = req.body().and_then(|body| body.size_hint().upper()) {
            if upper > self.0 {
                res.render(StatusError::payload_too_large());
                ctrl.skip_rest();
            } else {
                ctrl.call_next(req, depot, res).await;
            }
        }
    }
}