API serveur HTTP
Source: tutorial/06_http_server_api See in Playground
Un petit serveur HTTP avec trois routes : un point de terminaison de statut fixe, une route qui lit des métadonnées de requête depuis le contexte @HttpRequest, et une route qui analyse un corps JSON et répond avec un objet JSON construit à partir de celui-ci.
Exécution
cd tutorial/06_http_server_api
melodium run Compo.toml --port 8080curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/whoami
curl -X POST http://127.0.0.1:8080/greet -d '{"name":"Sláine"}'{"status":"ok"}
{"path":"/whoami","route":"/whoami"}
{"message":"thanks for the greeting!","received":"{\"name\":\"Sláine\"}"}Optionnel : ajoutez --api-report et un jeton d’API (MELODIUM_API_TOKEN) pour voir la trace complète de cette exécution sur Cadence.CI .
Fonctionnement
Un seul modèle HttpServer est partagé par les trois routes. start ouvre le socket une seule fois au démarrage, et connection est instanciée une fois par route, créant une nouvelle piste, avec le contexte @HttpRequest disponible, pour chaque requête entrante correspondante :
treatment main(const port: u16 = 8080)
model server: HttpServer(host=|from_ipv4(|localhost_ipv4()), port=port)
{
startup()
start[http_server=server]()
logReady: logInfoMessage(label="server", message="HTTP server ready")
startup.trigger -> start.trigger
startup.trigger -> logReady.trigger
health[http_server=server]()
whoami[http_server=server]()
greet[http_server=server]()
}health, whoami et greet sont des traitements indépendants, chacun possédant sa propre instance de connection ; main se contente d’instancier les trois contre le même modèle server. Le flux de données a la même forme pour chaque route :
Déclencher une réponse depuis connection.started
GET /health renvoie un statut JSON fixe :
treatment health[http_server: HttpServer]() {
connection[http_server=http_server](method=|get(), route="/health")
status: emit<HttpStatus>(value=|ok())
headers: emit<StringMap>(value=|map([]))
connection.started -> status.trigger,emit -> connection.status
connection.started -> headers.trigger,emit -> connection.headers
reply: emit<string>(value="{\"status\":\"ok\"}")
asBytes: stream<string>()
encoded: encode()
connection.started -> reply.trigger,emit -> asBytes.block,stream -> encoded.text,data -> connection.data
}La réponse est déclenchée par connection.started, un Block<void> qui se déclenche dès que la connexion est acceptée, plutôt que par un déclencheur dérivé de connection.data (le corps entrant). Une requête GET n’a pas de corps, donc aucun flux ne démarre jamais sur connection.data, et tout ce qui est conditionné par le “premier octet du corps” ne se déclencherait tout simplement jamais, laissant la route bloquée. C’est le point à ne jamais rater sur chaque route de ce type de code : /greet (une requête POST avec un vrai corps) fonctionnerait dans les deux cas, ce qui est justement le piège, puisqu’un déclencheur dérivé du corps paraît correct jusqu’à ce qu’il soit testé sur une route sans corps.
Lire les métadonnées de requête avec @HttpRequest
GET /whoami lit directement @HttpRequest[route] et @HttpRequest[path], sans toucher au corps de la requête :
treatment whoami[http_server: HttpServer]() {
connection[http_server=http_server](method=|get(), route="/whoami")
status: emit<HttpStatus>(value=|ok())
headers: emit<StringMap>(value=|map([]))
connection.started -> status.trigger,emit -> connection.status
connection.started -> headers.trigger,emit -> connection.headers
describe()
connection.started -> describe.trigger,body -> connection.data
}
treatment describe()
require @HttpRequest
input trigger: Block<void>
output body: Stream<byte>
{
info: emit<StringMap>(value=|insert(|entry("route", @HttpRequest[route]), "path", @HttpRequest[path]))
asJson: fromStringMap()
asText: toString<Json>()
asStream: stream<StringMap>()
encoded: encode()
Self.trigger -> info.trigger,emit -> asStream.block,stream -> asJson.value,json -> asText.value,into -> encoded.text,data -> Self.body
}describe déclare require @HttpRequest : Mélodium n’autorise donc son utilisation qu’à l’intérieur d’une piste qui fournit réellement ce contexte, ce que connection garantit.
Renvoyer un corps JSON analysé
POST /greet analyse le corps JSON et reconstruit une réponse avec entry / insert et fromStringMap :
treatment greet[http_server: HttpServer]() {
connection[http_server=http_server](method=|post(), route="/greet")
status: emit<HttpStatus>(value=|ok())
headers: emit<StringMap>(value=|map([]))
connection.started -> status.trigger,emit -> connection.status
connection.started -> headers.trigger,emit -> connection.headers
respondGreeting()
connection.data -> respondGreeting.data,data -> connection.data
}Il n’existe pas d’accès champ par champ à une valeur Json analysée dans le paquet json lui-même : pour extraire "Sláine" de {"name": "Sláine"}, il faut le moteur JavaScript, couvert dans l’étape suivante du tutoriel. Ici, tout le corps est renvoyé tel quel comme une seule valeur de chaîne JSON, sans y accéder champ par champ.
Dépendances
[dependencies]
std = "0.10.3" # flux de base, journalisation, structures de données
http = "0.10.3" # client et serveur HTTP
net = "0.10.3" # utilitaires d'adresses IP
json = "0.10.3" # parsing et sérialisation JSON
encoding = "0.10.3" # encodage / décodage UTF-8