Théorie poem 1 bases
Introduction
Poem est un socle d’applications (un framework) qui permet l’écriture d’API WEB. Il prévoit tous les outils dont on pourrait avoir besoin, ainsi que la possibilité d’apporter les nôtres. Ainsi on ne s’embêtera pas à gérer nous-même, le routage, la sérialisation, la dé-sérialisation, la validation, le CORS, les Cookies , les mesures anti-CSRF, etc.
Requête simple
Les fonctions qui gèrent les requêtes s’appellent des handlers (gérer == to handle). Dans son plus simple appareil un handler ressemble à ceci :
#[poem::handler]
fn index() -> &'static str {
"hello"
}
Le handler peut être monté a un endpoint (point-d’accès) comme ceci dans un serveur :
use poem::{Route, Server, listener::TcpListener, web, get};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
web_server_start().await;
Ok(())
}
#[tokio::main]
async fn web_server_start() -> anyhow::Result<()> {
let routes = Route::new()
.at("/", get(index));
Server::new(TcpListener::bind("0.0.0.0:3000")).run(routes).await?;
Ok(())
}
Lancez le programme et faites une requête sur le port 3000, vous devriez voir quelque chose de similaire :
> http :3000
HTTP/1.1 200 OK
content-length: 5
content-type: text/plain; charset=utf-8
date: Tue, 25 Feb 2025 15:47:40 GMT
hello
Recevoir un fragment du chemin en paramètre
Contrairement au NodeJS, nous n’allons pas (ou rarement) traiter un objet request ou response dans nos handlers. À la place on va juste écrire les paramètre dont on a besoin, avec des types spécifiques à poem en fonction de ce dont notre handler a besoin pour travailler. On les appelle des extracteurs, la liste des extracteurs disponibles par défaut se trouve ici
Pour extraire des fragments du chemin, on va utiliser un paramètre du type poem::web::Path avec en paramètre générique :
- le type de l’élément que l’on veut recevoir, s’il est tout seul ;
- un tuple avec les types de tous les éléments que l’on veut recevoir (dans l’ordre), s’ils sont plusieurs.
Les valeurs contenues dans le paramètre Path peuvent être extraites en le déconstruisant :
- en paramètre :
poem::web::Path(a, b, c): poem::web::Path<(String, i32, String)>:a,betcdeviennent des paramètres utilisables dans la fonction comme des variables ;
- en assignation :
let poem::web::Path(a, b, c) = path:- où
pathprovient d’un paramètrepath: poem::web::Path<(String, i32, String)>, a,betcdeviennent des variables utilisables dans la fonction.
- où
Exemples :
use poem::{Route, Server, get, listener::TcpListener, web::Path};
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let routes = Route::new()
.at("/:name", get(hello_name))
.at("/alt/:name", get(hello_name_alt))
.at("/:name/:year", get(hello_name_class));
Server::new(TcpListener::bind("0.0.0.0:3000"))
.run(routes)
.await
}
#[poem::handler]
fn hello_name(Path(name): Path<String>) -> String {
format!("hello {}", name)
}
#[poem::handler]
fn hello_name_alt(path: Path<String>) -> String {
let Path(name) = path;
format!("hello {}", name)
}
#[poem::handler]
fn hello_name_class(Path((name, year)): Path<(String, i32)>) -> String {
format!("hello {} student in year {}", name, year)
}
Renvoi de Json
Pour qu’une requête réponde quelque chose, il suffit de faire renvoyer ce quelque chose à un de nos handlers. Pour le moment on a renvoyé des chaînes de caractères. La liste complète de ce que l’on peut renvoyer par défaut se trouve ici
Si on le souhaite, on peut construire une réponse nous-même, et/ou implémenter IntoResponse nous même, sur une des nos structures. La plupart du temps on utilisera poem::Json pour renvoyer du JSON, dont la sérialisation se fait avec serde_json.
Pour que cela fonctionne vous devez ajouter au projet :
[dependencies]
serde = { version = "1.0.218", features = ["derive"] }
serde_json = "1.0.139"
use poem::web::Json;
#[poem::handler]
fn random_student() -> Json<Student> {
Json(Student {
name: "Toto".to_string(),
stream: Stream::CYB,
year: 1,
})
}
#[derive(serde::Serialize)]
enum Stream {
MKT,
CYB,
PRG,
AIB,
}
#[derive(serde::Serialize)]
struct Student {
name: String,
stream: Stream,
year: u8,
}
Recevoir du JSON
En appliquant la logique des extracteurs à poem::web::Json on peut recevoir un objet JSON depuis le corps d’une requête et remplir une structure avec ses données. La structure doit #[derive(serde::Deserialize)] pour que cela fonctionne.
La fonction pass_student_through vous donne un exemple de handler qui reçois un Student extrait depuis le corps de la requête, le modifie, puis le renvoie.
use poem::{Route, Server, get, listener::TcpListener, web::Path};
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let routes = Route::new()
.at("/student", get(random_student).post(pass_student_through));
Server::new(TcpListener::bind("0.0.0.0:3000"))
.run(routes)
.await
}
use poem::web::Json;
// [handler random_student]
#[poem::handler]
fn pass_student_through(Json(mut student): Json<Student>) -> Json<Student> {
student.name = student.name.to_uppercase();
student.year += 1;
Json(student)
}
#[derive(serde::Serialize, serde::Deserialize)]
enum Stream {
MKT,
CYB,
PRG,
AIB,
}
#[derive(serde::Serialize, serde::Deserialize)]
struct Student {
name: String,
stream: Stream,
year: u8,
}
Exemple de requête avec HTTPie :
> http :3000/student name=toto stream=CYB year:=2
HTTP/1.1 200 OK
content-length: 39
content-type: application/json; charset=utf-8
date: Thu, 27 Feb 2025 15:17:05 GMT
{
"name": "TOTO",
"stream": "CYB",
"year": 3
}
Recevoir un contexte ?
Quand on travaille avec une API, il y a souvent des données que l’on veut utiliser dans, voir modifier depuis, nos handlers. Ça peut être une connexion a une base de données, de la configuration, un service métier, etc.
Depuis un handler on peut recevoir des données ne provenant pas de la requête en écrivant :
#[derive(Clone)]
struct ExampleConfig {
mode: String,
}
use poem::web::Data;
#[poem::handler]
fn example_with_context(Data(context): Data<&ExampleConfig>) -> String {
format!("mode {}", context.mode)
}
La donnée doit être passée au routeur avec .data() (rendu disponible par poem::EndpointExt).
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let conf = ExampleConfig {
mode: "dev config".to_string(),
};
use poem::{EndpointExt, get};
let routes = poem::Route::new()
.at("/ctx", get(example_with_context))
.data(conf);
poem::Server::new(poem::listener::TcpListener::bind("0.0.0.0:3000"))
.run(routes)
.await
}
poem sait quelles données envoyer au handler en fonction du type demandé. Ainsi vous ne pouvez pas différencier deux contextes du même type.
Modifier un contexte, exclusion mutuelle
Mettons vous voulez modifier un contexte, pour suivre le nombre de visite de votre API par exemple. Sauf que le contexte reçu ne peut pas être modifié, du fait d’être potentiellement partagé par plusieurs fils d’exécution.
Pour pouvoir modifier un contexte on doit utiliser des concepts qui s’appellent :
- le comptage de références (reference counting) ;
- la mutabilité interne (internal mutability) (comme avec
RefCell) ; et - la synchronisation (comme avec des
Mutexou des variables atomiques).
Exemple avec un AtomicUsize :
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
struct ExampleCtx {
mode: String,
visits: AtomicUsize,
}
#[derive(Clone)]
struct ExampleConfig {
mode: String,
}
use poem::web::Data;
#[poem::handler]
fn example_with_context(Data(context): Data<&ExampleConfig>) -> String {
format!("mode {}", context.mode)
}
#[poem::handler]
fn example_with_context_and_visits(Data(context): Data<&Arc<ExampleCtx>>) -> String {
use std::sync::atomic::Ordering::SeqCst;
context.visits.fetch_add(1, SeqCst);
let visits = context.visits.load(SeqCst);
format!("mode {} with visits: {}", context.mode, visits)
}
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
let conf = ExampleConfig {
mode: "dev config".to_string(),
};
let context = Arc::new(ExampleCtx {
mode: "dev context".to_string(),
visits: AtomicUsize::new(0),
});
use poem::{EndpointExt, get};
let routes = poem::Route::new()
.at("/ctx", get(example_with_context))
.at("/vis", get(example_with_context_and_visits))
.data(context)
.data(conf);
poem::Server::new(poem::listener::TcpListener::bind("0.0.0.0:3000"))
.run(routes)
.await
}
Si la donnée que l’on veut stocker et modifier est plus complexe qu’un nombre, on va plutôt la ranger dans :
- une
std::sync::Mutex; ou - un
std::sync::RwLock.
Traçabilité
Dans TD Instrumentalisation tracing nous avons vu l’avantage des portées (spans) pour contextualiser les événements de log émis dans le programme. Poem supporte la création de portées spécifiques à chacune de requête. Ainsi : on sait toujours pour quelle requête un log à été émis.
poem::middleware::Tracing ajoute le contexte de la requête aux événements produits pour elle : IP, méthode HTTP, URI.
poem::middleware::RequestId ajoute un identifiant alphanumérique à chaque requête, pour la rendre plus facilement… identifiable. L’identifiant est visible dans les logs ET en réponse de la requête, sur un header x-request-id qu’un utilisateur peut vous donner s’il a un problème.
Exemple de main.rs qui :
- configure
tracing_subscriberavec le log en json dans un fichierlogs/app.jsonl; - configure
tracing_subscriberavec le log en pretty sur la sortie standard ; - démarre un server
poemavec les middlewares de Tracing et de RequestId.
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let _json_logs_guard = tracing_setup()?;
web_server_start().await?;
Ok(())
}
fn tracing_setup() -> anyhow::Result<tracing_appender::non_blocking::WorkerGuard> {
std::fs::create_dir_all("logs")?;
let json_file = tracing_appender::rolling::never("logs", "app.jsonl");
let (json_writer, json_guard) = tracing_appender::non_blocking(json_file);
let stdout_layer = tracing_subscriber::fmt::layer()
.with_writer(std::io::stdout)
.with_target(false)
.with_ansi(true)
.pretty();
let json_layer = tracing_subscriber::fmt::layer()
.with_writer(json_writer)
.with_ansi(false)
.with_target(true)
.json();
tracing_subscriber::registry()
.with(stdout_layer)
.with(json_layer)
.with(tracing_subscriber::filter::LevelFilter::TRACE)
.init();
Ok(json_guard)
}
pub async fn web_server_start(db: sqlx::PgPool) -> anyhow::Result<()> {
let api_routes = poem::Route::new()
.at("/", get(index))
.with(poem::middleware::Tracing::default())
.to_response() // on ajoute RequestId après un `to_response` autrement le x-request-id n'est pas ajouter aux requêtes qui ont une erreur (ça serait dommage)
.with(poem::middleware::RequestId::default());
info!("starting server");
poem::Server::new(poem::listener::TcpListener::bind("0.0.0.0:3000"))
.run(api_routes)
.await?;
Ok(())
}
Exemple de requête :
● http :3001/password_auth/login email_addr=elise@ecole-89.com clear_password=ab
HTTP/1.1 401 Unauthorized
content-length: 22
content-type: text/plain; charset=utf-8
date: Mon, 30 Mar 2026 08:05:45 GMT
x-request-id: d49a920b-731c-4fb5-8300-d4ff37001004
verifying the password
Et les logs qu’elle peut produire :
2026-03-30T08:01:45.778861Z INFO hecate::db: return: PasswordAuth {
pwauth_email: "elise@ecole-89.com",
pwauth_hash: "$argon2id$v=19$m=19456,t=2,p=...", profile_id: Some(2)
}
at src/db.rs:68
in hecate::db::password_auth_get with email: "elise@ecole-89.com"
in poem::middleware::tracing_mw::request with remote_addr: 127.0.0.1, version: HTTP/1.1, method: POST, uri: /password_auth/login, request_id: d49a920b-731c-4fb5-8300-d4ff37001004
in poem::middleware::requestid:: with request_id: d49a920b-731c-4fb5-8300-d4ff37001004
2026-03-30T08:01:46.519364Z WARN hecate::web_server: err: Password
at src/web_server.rs:203
in poem::middleware::tracing_mw::request with remote_addr: 127.0.0.1, version: HTTP/1.1, method: POST, uri: /password_auth/login, request_id: d49a920b-731c-4fb5-8300-d4ff37001004
in poem::middleware::requestid:: with request_id: d49a920b-731c-4fb5-8300-d4ff37001004
2026-03-30T08:01:46.519574Z INFO poem::middleware::tracing_mw: error, status: 401 Unauthorized, error: verifying the password, duration: 743.133731ms
at /home/eriizu/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/poem-3.1.12/src/middleware/tracing_mw.rs:99
in poem::middleware::tracing_mw::request with remote_addr: 127.0.0.1, version: HTTP/1.1, method: POST, uri: /password_auth/login, request_id: d49a920b-731c-4fb5-8300-d4ff37001004
in poem::middleware::requestid:: with request_id: d49a920b-731c-4fb5-8300-d4ff37001004
Et enfin, un petit script pour que je vous donne en cadeau, qui peut :
- vous lister les requêtes qui ont eu lieu dans vos logs ;
- vous donner les événements qui ont eu lieu lors d’une requête.
Exemple d’utilisation :
cat logs/app.jsonl | sh ./logs_by_request_id.sh
cat logs/app.jsonl | sh ./logs_by_request_id.sh d49a920b-731c-4fb5-8300-d4ff37001004