ypack gallery Plugins YEditor  ·  Télécharger YScript  ·  Télécharger YEditor

← Tous les paquets

http v0.3.0

Client HTTP module for YScript

Installation
yes.exe ypack install http@0.3.0
AuteurYan Grenier
Publié le2026-08-10T11:41:40+00:00
Checksumsha256:093d8038602837bcb133718a2b8633675fb1e19b70f525470ad4229ecb1e7fa9
Moteur requis
yscript >=0.3.0
Fournit module

Dépendances

Aucune dépendance.

Téléchargement

Archive .zip de la version 0.3.0

Historique des versions

Version Date
0.3.0 (dernière) 2026-08-10T11:41:40+00:00

Documentation

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éthodeDescription
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

majuscules quel que soit ce qui est passé. url doit être une url absolue non vide.

retournent directement une réponse, pas l'objet requête :

options (table optionnelle) peut contenir headers, query, timeout, contenttype, fromfile, tofile — voir les sections correspondantes plus bas ; c'est un raccourci pour ne pas passer par http.request(...) + réglage des propriétés + :send() quand la requête est simple.

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éTypeDescription
methodstringMéthode HTTP, normalisée en majuscules
urlstringURL de base (sans les paramètres de query)
headerstable{ [nom] = valeur }
querytable{ [nom] = valeur }, sérialisés en ?nom=valeur&...
bodystring | table | nilCorps de la requête ; voir « Sérialisation du corps » ci-dessous
fromfilestring | FILE* | nilLit le corps à envoyer depuis un fichier plutôt que body
contenttypestring | nilForce le Content-Type ; déduit de body/fromfile sinon
timeoutinteger | nilTimeout 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)

précisé.

que le module json soit chargeable (require("json")), sinon erreur.

en chaîne dans ce cas.

-- 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

réponse, erreur ou timeout) et retourne une réponse. Un échec réseau/DNS/timeout lève une erreur script (capturable avec pcall) ; un code de statut d'erreur (4xx/5xx) n'est pas une erreur — c'est une réponse normale avec res.ok == false.

fois (nouvel appel réseau avec l'état courant de la requête), mais modifier les propriétés après un premier send() n'a aucun effet rétroactif sur la réponse déjà reçue.

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.

  1. **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.

MembreTypeDescription
statusintegerCode de statut HTTP (200, 404, ...)
statustextstringTexte associé ("OK", "Not Found", ...)
okbooleantrue si status est dans [200, 300)
headerstable{ [nom] = valeur } des headers de réponse
urlstringURL finale (après redirections éventuelles)
bodystring | nilCorps de la réponse, décodé en texte selon son Content-Type. nil si tofile a été utilisé, "" si le corps est vide
filestring | FILE* | nilLa valeur tofile telle que passée à send() (présente uniquement quand tofile a été utilisé)
requestRequest | nilLa requête à l'origine de cette réponse — nil pour une réponse obtenue en asynchrone (voir « API asynchrone »)
response:json()méthodejson.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

client.baseurl.

Propriétés

PropriétéTypeDescription
baseurlstring | nilPréfixée aux chemins relatifs passés aux méthodes du client
headerstableHeaders par défaut, fusionnés dans chaque requête (la requête gagne en cas de collision)
timeoutinteger | nilTimeout 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

dans l'ordre d'enregistrement, juste avant l'envoi, avec la requête déjà résolue (url absolue, headers fusionnés). Un handler modifie req en place ; aucune valeur de retour attendue.

(relancer req:send() en cas d'échec) reste à la charge du script — un handler n'a accès qu'à la requête sortante, pas à la réponse.

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 :

asynchrones des raccourcis du module.

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

standard de coroutine.resume (erreur remontée comme second résultat, pas d'exception), à la différence du req:send() synchrone qui, lui, lève une vraie erreur script.

le soit.

http.ready(co) : sonder sans bloquer

signifie que le prochain coroutine.resume(co) retournera immédiatement.

créé par ce module, est une erreur.

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

(le reste du module fonctionne sans elle). Doit être chargée par l'hôte (OpenIOLib) ou par le script (require("io")) avant d'utiliser tofile/fromfile ; sinon, erreur explicite à l'usage (pas au chargement du module http lui-même, contrairement à zip).

(par défaut) et pour response:json(). Chargée à la demande via require("json") ; si elle n'est pas trouvable au moment où une de ces fonctionnalités est utilisée, erreur explicite.


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.

SituationComportement
method/url vide ou invalideerreur 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/tableerreur script
body et fromfile renseignés tous les deux sur la même requêteerreur script
tofile/fromfile utilisé sans que io soit chargéeerreur script
tofile/fromfile d'un type autre que nil/string/FILE*erreur script
FILE* fourni à tofile fermé ou pas ouvert en écritureerreur script
FILE* fourni à fromfile fermé ou pas ouvert en lectureerreur script
response:json() sur un body nil ou non-JSONerreur script
body/response:json() nécessitant json alors qu'il n'est pas chargeableerreur script
http.ready(co) sur un thread mort ou étranger à ce moduleerreur 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

contenu binaire volumineux est tofile.

streaming réel dans ce cas précis — voir « API asynchrone »).