Module http

Le module http permet de faire des requêtes HTTP depuis un script YScript, dans l'esprit de HttpClient en .NET : appels ponctuels, objet client réutilisable avec headers/handlers, et variante asynchrone pilotée par coroutines.

local http = require("http")

local res = http.get("https://api.example.com/status")
print(res.status, res.ok, res.body)

Ce module ne fournit pas de serveur HTTP, ni de WebSocket ; c'est un client HTTP scriptable.

Vue d'ensemble

Fonction / méthode Description
http.request(method, url) Crée une requête (non envoyée)
http.get/post/put/patch/delete/head(url [, ...] [, options]) Raccourcis requête + envoi immédiat, retournent une réponse
http.getasync/postasync/.../headasync(...) Mêmes raccourcis en asynchrone, retournent une coroutine
http.ready(co) Teste si une coroutine http async est prête, sans bloquer
http.client([baseurl]) Crée un client réutilisable (url de base, headers, handlers)
request:send([tofile]) Envoie une requête, retourne une réponse
request:sendasync([tofile]) Envoie une requête en asynchrone, retourne une coroutine
client:get/post/put/patch/delete/head(path [, ...] [, options]) Mêmes raccourcis que le module, relatifs à client.baseurl
client:request(method, path [, options]) Équivalent générique pour une méthode sans raccourci dédié
client:usehandler(fn) Enregistre un middleware appelé sur chaque requête avant envoi
response:json() Décode response.body en JSON (nécessite le module json)

L'objet requête

http.request crée une requête sans l'envoyer, pour pouvoir l'inspecter/modifier avant send() :

local req = http.request("GET", "https://api.example.com/users")
req.headers["Accept"] = "application/json"
req.query.page = 2
req.timeout = 5000 -- ms

local res = req:send()

Construction

local res = http.get("https://api.example.com/search", {
    query = { q = "yscript" },
    headers = { Accept = "application/json" },
    timeout = 3000,
})

Propriétés (lecture/écriture avant envoi)

Propriété Type Description
method string Méthode HTTP, normalisée en majuscules
url string URL de base (sans les paramètres de query)
headers table { [nom] = valeur }
query table { [nom] = valeur }, sérialisés en ?nom=valeur&...
body string | table | nil Corps de la requête ; voir « Sérialisation du corps » ci-dessous
fromfile string | FILE* | nil Lit le corps à envoyer depuis un fichier plutôt que body
contenttype string | nil Force le Content-Type ; déduit de body/fromfile sinon
timeout integer | nil Timeout en millisecondes ; nil = pas de limite

body et fromfile sont mutuellement exclusifs : affecter l'un après avoir déjà renseigné l'autre est une erreur — pas de résolution silencieuse de conflit.

local req = http.request("POST", "https://api.example.com/upload")
req.body = "hello"
req.fromfile = "data.bin"   -- erreur : 'body' est déjà renseigné

⚠️ Les entêtes Content-Type/Content-Length placées dans req.headers sont silencieusement ignorées à l'envoi : utilisez req.contenttype pour contrôler le Content-Type, Content-Length étant toujours calculé automatiquement.

Sérialisation du corps (body)

-- table -> JSON par défaut
http.post(url, { name = "Yanos", age = 42 })

-- table -> formulaire
http.post(url, { username = "yanos", password = "secret" },
    { contenttype = "application/x-www-form-urlencoded" })

Envoi

Enregistrer directement la réponse dans un fichier (tofile)

Pour télécharger une réponse potentiellement volumineuse (fichier, archive, image) sans la charger intégralement en mémoire dans res.body, passez tofile à send()/sendasync() (pas une propriété de la requête — c'est un paramètre de l'envoi, puisque ça concerne la réponse) :

require("io")
local res = http.request("GET", "https://example.com/archive.zip"):send("archive.zip")
print(res.status, res.body)   -- res.body == nil : le contenu est dans archive.zip

tofile accepte deux formes :

  1. une chaîne (chemin de fichier) — le module l'ouvre lui-même (io.open(chemin, "wb")), écrit le corps reçu en streaming au fil de la réception réseau, puis le referme systématiquement (succès comme échec). Une erreur réseau en cours de transfert referme le fichier mais conserve le contenu partiel déjà reçu — pas de suppression implicite.
  2. un FILE* déjà ouvert par le script en écriture — le module écrit dedans mais ne le ferme jamais (comme io.write) ; utile pour écrire plusieurs réponses à la suite dans le même fichier.
local f = io.open("archive.zip", "wb")
http.request("GET", url1):send(f)
http.request("GET", url2):send(f)   -- ajouté à la suite dans le même fichier
f:close()

Cette fonctionnalité nécessite la bibliothèque io (voir « Dépendances optionnelles » plus bas).

Lire le corps à envoyer depuis un fichier (req.fromfile)

Symétrique de tofile, côté requête cette fois : envoyer un fichier local en upload (PUT/POST d'une image, d'une archive, ...) sans le charger intégralement en mémoire dans req.body.

require("io")
local req = http.request("PUT", "https://api.example.com/files/archive.zip")
req.fromfile = "archive.zip"          -- ou : req.fromfile = io.open("archive.zip", "rb")
local res = req:send()

Comme tofile, req.fromfile accepte une chaîne (le module ouvre/referme lui-même le fichier en lecture) ou un FILE* déjà ouvert en lecture par le script (jamais refermé par le module). Si contenttype n'est pas précisé, application/octet-stream est utilisé par défaut.


L'objet réponse

Retourné par req:send()/req:sendasync() et par les raccourcis http.get/http.post/etc.

Membre Type Description
status integer Code de statut HTTP (200, 404, ...)
statustext string Texte associé ("OK", "Not Found", ...)
ok boolean true si status est dans [200, 300)
headers table { [nom] = valeur } des headers de réponse
url string URL finale (après redirections éventuelles)
body string | nil Corps de la réponse, décodé en texte selon son Content-Type. nil si tofile a été utilisé, "" si le corps est vide
file string | FILE* | nil La valeur tofile telle que passée à send() (présente uniquement quand tofile a été utilisé)
request Request | nil La requête à l'origine de cette réponse — nil pour une réponse obtenue en asynchrone (voir « API asynchrone »)
response:json() méthode json.decode(response.body) ; erreur si body est nil (réponse déviée vers un fichier) ou n'est pas du JSON valide
local res = http.get("https://api.example.com/users/42")
if res.ok then
    local user = res:json()
    print(user.name)
else
    print("échec :", res.status, res.statustext)
end

Client

Un client fixe un socle commun (url de base, headers par défaut, timeout par défaut) pour une série d'appels, et sert de point d'extension pour construire un petit SDK au-dessus de http.

local api = http.client("https://api.example.com")
api.headers["Authorization"] = "Bearer " .. token

-- Handler : modifie la requête juste avant envoi (auth, id de corrélation, logging...)
api:usehandler(function(req)
    req.headers["X-Request-Id"] = tostring(os.time())
end)

-- Extension "SDK" : méthode métier au-dessus du client générique
function api:getuser(id)
    return self:get("/users/" .. id):json()
end

local user = api:getuser(42)

Construction

Propriétés

Propriété Type Description
baseurl string | nil Préfixée aux chemins relatifs passés aux méthodes du client
headers table Headers par défaut, fusionnés dans chaque requête (la requête gagne en cas de collision)
timeout integer | nil Timeout par défaut pour les requêtes de ce client (si l'appel n'en précise pas)

Un chemin passé à une méthode du client est résolu contre client.baseurl s'il est relatif ; s'il contient déjà :// (url absolue), baseurl est ignorée pour cet appel.

Méthodes de requête

Mêmes raccourcis que le module : client:get(path [, options]), client:post(path, body [, options]), client:put, client:patch, client:delete, client:head — envoient immédiatement et retournent une réponse, comme leurs équivalents http.*. client:request(method, path [, options]) est l'équivalent générique pour une méthode non couverte par un raccourci.

Chaque appel : fusionne client.headers puis les options.headers de l'appel (l'appel gagne), applique client.timeout si l'appel n'en précise pas, exécute la chaîne de handlers, puis envoie.

ℹ️ Il n'existe pas de raccourci asynchrone au niveau client (client:getasync n'existe pas). Pour envoyer une requête construite via un client de façon asynchrone, construisez-la vous-même avec http.request(method, url) (en résolvant l'url manuellement) puis appelez :sendasync() — voir « API asynchrone ».

Handlers

Extension en SDK

Un client se comporte comme une table pour tout ce qui n'est pas une propriété/méthode native : poser un champ (fonction ou non) dessus fonctionne exactement comme sur une table normale, ce qui permet de construire un petit SDK dédié à une API par-dessus le client générique :

local api = http.client("https://api.example.com")

function api:getuser(id)
    local res = self:get("/users/" .. id)
    if not res.ok then error("getuser failed: " .. res.status) end
    return res:json()
end

api.appname = "myapp"   -- champ de donnée arbitraire, lu/écrit comme sur une table normale

Seules les clés de type chaîne sont prises en charge sur un client (une clé d'un autre type lève une erreur).


API asynchrone

Chaque envoi a un pendant asynchrone qui retourne un thread YScript (le type déjà connu de coroutine.*) au lieu d'une réponse directement :

La requête réseau démarre immédiatement à la création (pas d'attente d'un premier resume) — important pour lancer plusieurs téléchargements réellement en parallèle.

coroutine.resume

local co1 = http.getasync("https://example.com/a")
local co2 = http.getasync("https://example.com/b")

local ok1, res1 = coroutine.resume(co1)  -- bloque jusqu'à ce que la requête 1 soit terminée
local ok2, res2 = coroutine.resume(co2)  -- déjà résolue entre-temps -> immédiat

http.ready(co) : sonder sans bloquer

local jobs = {}
for i, url in ipairs(urls) do
    jobs[i] = http.getasync(url)   -- démarre les N appels réseau immédiatement
end

while true do
    local allready = true
    for _, job in ipairs(jobs) do
        if not http.ready(job) then allready = false; break end
    end
    if allready then break end
    coroutine.yield()   -- utile si ce code tourne lui-même dans une coroutine hôte
end

for i, job in ipairs(jobs) do
    local ok, res = coroutine.resume(job)
    print(urls[i], ok and res.status or ("error: " .. res))
end

Note : sur une réponse obtenue en asynchrone, res.request vaut toujours nil (aucune référence à la requête ne survit au passage par le thread d'arrière-plan) ; et un tofile combiné à :sendasync() bufférise le corps entier en mémoire côté arrière-plan avant de l'écrire au resume (contrairement à req:send(tofile) synchrone, qui écrit vraiment en streaming) — à garder en tête pour de très gros téléchargements async.


Dépendances optionnelles


Erreurs

Les échecs réseau (req:send()) et de programmation (méthode/url invalide, type de body incorrect, conflit body/fromfile, ...) sont levés comme des erreurs script — à capturer avec pcall si nécessaire. Un code de statut HTTP 4xx/5xx n'est jamais une erreur : c'est une réponse normale avec ok == false.

Situation Comportement
method/url vide ou invalide erreur script
Échec réseau/DNS/timeout sur req:send() erreur script
body de type table avec un contenttype non supporté erreur script
body d'un type autre que nil/string/table erreur script
body et fromfile renseignés tous les deux sur la même requête erreur script
tofile/fromfile utilisé sans que io soit chargée erreur script
tofile/fromfile d'un type autre que nil/string/FILE* erreur script
FILE* fourni à tofile fermé ou pas ouvert en écriture erreur script
FILE* fourni à fromfile fermé ou pas ouvert en lecture erreur script
response:json() sur un body nil ou non-JSON erreur script
body/response:json() nécessitant json alors qu'il n'est pas chargeable erreur script
http.ready(co) sur un thread mort ou étranger à ce module erreur script
Poser une clé non-chaîne sur un client (client[1] = ...) erreur script

Exemple complet

local http = require("http")

-- Appel simple
local res = http.get("https://api.example.com/status")
print(res.status, res.ok)

-- Client + handler + extension SDK
local api = http.client("https://api.example.com")
api.headers["Authorization"] = "Bearer " .. token
api:usehandler(function(req)
    req.headers["X-Trace-Id"] = tostring(os.time())
end)

function api:getuser(id)
    local res = self:get("/users/" .. id)
    if not res.ok then
        error("getuser failed: " .. res.status)
    end
    return res:json()
end

local user = api:getuser(42)

-- Téléchargement direct vers un fichier
require("io")
local dl = http.request("GET", "https://example.com/archive.zip")
local dres = dl:send("archive.zip")          -- ou : dl:send(io.open("archive.zip", "wb"))
print(dres.status, dres.body)                -- dres.body == nil, le contenu est dans archive.zip

-- Upload direct depuis un fichier
local up = http.request("PUT", "https://api.example.com/files/archive.zip")
up.fromfile = "archive.zip"                  -- ou : up.fromfile = io.open("archive.zip", "rb")
local ures = up:send()

-- Téléchargements en parallèle
local urls = { "https://a.example.com", "https://b.example.com", "https://c.example.com" }
local jobs = {}
for i, url in ipairs(urls) do
    jobs[i] = http.getasync(url)
end
for i, job in ipairs(jobs) do
    local ok, r = coroutine.resume(job)
    if ok then
        print(urls[i], r.status)
    else
        print(urls[i], "error:", r)
    end
end

Limites connues