Théorie poem 3 gestion d'erreur
Construction manuelle d’une réponse
Un handler qui peut échouer a plusieurs stratégies pour le faire savoir :
- construire ces réponses à la main et renvoyer un
poem::Responseouimpl poem::IntoResponse; - renvoyer un
Result:- dont la variante
Okimplémentepoem::IntoResponse(commepoem::web::Json,String, etc.) et ; - dont la variante
Errimplémentepoem::ResponseError.
- dont la variante
Dans les deux cas on va apprendre à fabriquer des requêtes à la mano, mais dans le second, on va surtout apprendre à ne pas se répéter. Si on gère une erreur une fois, on a aura plus jamais besoin de se répéter.
Exemple en créant la réponse à la mano :
mod hdl {
use crate::ExampleCtx;
use crate::model;
use poem::IntoResponse;
use poem::web::{Data, Json, Path};
use std::sync::Arc;
#[poem::handler]
pub fn insert_message(
Json(message): Json<model::MessageInputDto>,
Data(context): Data<&Arc<ExampleCtx>>,
) -> poem::Response {
let Some(id) = context.insert_message(message) else {
return poem::Response::builder()
.status(poem::http::StatusCode::INTERNAL_SERVER_ERROR)
.finish();
};
let Some(msg) = context.get_message(id) else {
return poem::Response::builder()
.status(poem::http::StatusCode::INTERNAL_SERVER_ERROR)
.finish();
};
Json(model::MessageOutputDto {
text: msg.text,
created_on: msg.created_on,
id,
})
.into_response()
}
}
C’est un peu chargé. Et ça peut devenir très chargé si on doit gérer différents types d’erreurs qui veulent dire différent type de choses. Tout dans la vie d’un serveur n’est pas une INTERNAL_SERVER_ERROR. Parfois c’est l’utilisateur qu’il faut disputer parce qu’il essaie de créer une donnée en conflit avec une autre, ou parce qu’il tente d’obtenir une autre donnée qui n’existe pas.
Limiter la duplication de code
Heureusement, on a pas besoin d’écrire le code de création de requête partout. On peut l’écrire à un seul endroit. Dans une implémentation de poem::ResponseError que l’on va faire sur un type de notre création.
Ce type d’erreur on va l’appeler MyHandlerError pour l’exemple. Nos handlers vont pouvoir renvoyer Result<Json<qqchose>, MyHandlerError>.
On avait vu précédemment comment le crate derive_more pouvais nous faciliter la tâche pour stocker des erreurs dans une énumération. Un autre crate qui nous facilite encore plus la tâche c’est thiserror. Il permet de générer pour nous tout le code pour implémenter std::error::Error.
Prenons pour exemple deux cas qui peuvent nous arriver :
- on ne trouve pas une entité (un message, un utilisateur, etc.)
- il se passe quelque chose qu’on ne sais pas gérer.
#[derive(Debug, thiserror::Error)]
enum MyHandlerError {
#[error("{entity} not found")]
NotFound { entity: &'static str },
#[error("unexpected issue while handling the request, context: {context}")]
Unexpected { context: &'static str },
}
Pour que nos handlers aient le droit de renvoyer notre énumération d’erreur elle doit implémenter poem::error::ResponseError.
Il n’y a que deux méthodes à implémenter :
statusqui nous permet d’associer un code erreur à chaque variante de notre enum (voir à d’autres conditions si nécessaire) ;as_responsequi crée la réponse avec le status et pour corps l’erreur transformée en chaîne de caractère (c’est ici que l’on pourrait changer la présentation de l’erreur, la mettre dans duJson, insérer des détails si on en avait, etc).
impl poem::error::ResponseError for MyHandlerError {
fn status(&self) -> poem::http::StatusCode {
use poem::http::StatusCode;
match self {
Self::Unexpected { context: _ } => StatusCode::INTERNAL_SERVER_ERROR,
Self::NotFound { entity: _ } => StatusCode::NOT_FOUND,
}
}
fn as_response(&self) -> poem::Response
where
Self: std::error::Error + Send + Sync + 'static,
{
poem::Response::builder()
.status(self.status())
.body(self.to_string())
}
}
Enfin on peut modifier nos handlers pour qu’ils renvoient un Result :
mod hdl {
// [...]
use super::MyHandlerError;
#[poem::handler]
pub fn insert_message(
Json(message): Json<model::MessageInputDto>,
Data(context): Data<&Arc<ExampleCtx>>,
) -> Result<Json<model::MessageOutputDto>, MyHandlerError> {
let id = context.insert_message(message).ok_or(MyHandlerError::Unexpected{context: "insertion"})?;
let msg = context.get_message(id).ok_or(MyHandlerError::Unexpected{ context: "fetching inserted message" })?;
Ok(Json(model::MessageOutputDto {
text: msg.text,
created_on: msg.created_on,
id,
}))
}
#[poem::handler]
pub fn get_message(
Path(id): Path<usize>,
Data(context): Data<&Arc<ExampleCtx>>,
) -> Result<Json<model::MessageOutputDto>, MyHandlerError> {
let msg = context.get_message(id).ok_or(MyHandlerError::NotFound{ entity: "message" })?;
Ok(Json(model::MessageOutputDto {
text: msg.text,
created_on: msg.created_on,
id,
}))
}
}
Intégration avec une base de données
Pour pouvoir faire des requêtes a une base de donnée depuis vos handlers, vous pouvez :
- passer directement la
sqlx::PgPoolen.dataaupoem::Route; - ajouter la
sqlx::PgPoolà une structure passée endataaupoem::Route.
Les erreurs de base de donnée sqlx::Error peuvent ajoutées en variante à votre énumération d’erreur.
#[derive(Debug, thiserror::Error)]
enum MyHandlerError {
#[error("{entity} not found")]
NotFound { entity: &'static str },
#[error("unexpected issue while handling the request, context: {context}")]
Unexpected { context: &'static str },
#[error(transparent)]
Db(#[from] sqlx::Error),
}
Regardez sa documentation pour voir à quels codes HTTP peuvent être associées les erreurs d’sqlx. Modifiez votre implémentation de poem::error::ResponseError pour prendre en compte les sqlx::Error.