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
http.request(method, url)—methodest une chaîne ("GET","POST", ...), normalisée en majuscules quel que soit ce qui est passé.urldoit être une url absolue non vide.Raccourcis équivalents à
http.request(<méthode>, url)suivi d'un envoi immédiat — ils retournent directement une réponse, pas l'objet requête :http.get(url [, options]),http.head(url [, options]),http.delete(url [, options])http.post(url, body [, options]),http.put(url, body [, options]),http.patch(url, body [, options])
options(table optionnelle) peut contenirheaders,query,timeout,contenttype,fromfile,tofile— voir les sections correspondantes plus bas ; c'est un raccourci pour ne pas passer parhttp.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é | 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)
nil— pas de corps (défaut pourGET/HEAD/DELETE), sauf sifromfileest renseigné.string— envoyée telle quelle, encodée en UTF-8 ;contenttypepar défauttext/plainsi non précisé.table— encodée seloncontenttype:application/json(défaut sicontenttypen'est pas précisé) :json.encode(body)— nécessite que le modulejsonsoit chargeable (require("json")), sinon erreur.application/x-www-form-urlencoded: encodagenom=valeur&...à partir des paires de la table.- Toute autre valeur de
contenttypeavec unbodyde type table est une erreur — pré-sérialisez 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
req:send([tofile])— envoie la requête de façon synchrone (bloque le script courant jusqu'à réponse, erreur ou timeout) et retourne une réponse. Un échec réseau/DNS/timeout lève une erreur script (capturable avecpcall) ; un code de statut d'erreur (4xx/5xx) n'est pas une erreur — c'est une réponse normale avecres.ok == false.req:sendasync([tofile])— variante asynchrone, voir « API asynchrone » plus bas.- Une requête est à usage unique dans l'esprit : rien n'empêche de rappeler
:send()une deuxième fois (nouvel appel réseau avec l'état courant de la requête), mais modifier les propriétés après un premiersend()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 :
- 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. - un
FILE*déjà ouvert par le script en écriture — le module écrit dedans mais ne le ferme jamais (commeio.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
http.client([baseurl])—baseurlest optionnelle, peut être fixée/changée ensuite viaclient.baseurl.
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:getasyncn'existe pas). Pour envoyer une requête construite via un client de façon asynchrone, construisez-la vous-même avechttp.request(method, url)(en résolvant l'url manuellement) puis appelez:sendasync()— voir « API asynchrone ».
Handlers
client:usehandler(fn)— enregistrefn(req)dans la chaîne du client. Chaque handler est appelé, dans l'ordre d'enregistrement, juste avant l'envoi, avec la requête déjà résolue (url absolue, headers fusionnés). Un handler modifiereqen place ; aucune valeur de retour attendue.- Usage typique : authentification, id de corrélation, journalisation. La gestion de retry
(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 :
http.getasync(url [, options]),http.postasync(url, body [, options]), ... — pendants asynchrones des raccourcis du module.req:sendasync([tofile])— pendant asynchrone dereq:send().
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
- Si la requête a réussi :
coroutine.resumeretournetrue, response. - Si elle a échoué (réseau, timeout) : retourne
false, message— cohérent avec la sémantique standard decoroutine.resume(erreur remontée comme second résultat, pas d'exception), à la différence dureq:send()synchrone qui, lui, lève une vraie erreur script. - Si la requête n'est pas encore terminée,
resumeattend (bloque l'appelant) jusqu'à ce qu'elle le soit.
http.ready(co) : sonder sans bloquer
http.ready(co)retournetrue/false, sans jamais bloquer ni faire progresserco.truesignifie que le prochaincoroutine.resume(co)retournera immédiatement.- Appeler
http.readysur un thread déjà consommé (donc"dead"), ou sur un thread qui n'a pas été 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
io— nécessaire uniquement pourtofile/req.fromfilesous forme de chemin de fichier (le reste du module fonctionne sans elle). Doit être chargée par l'hôte (OpenIOLib) ou par le script (require("io")) avant d'utilisertofile/fromfile; sinon, erreur explicite à l'usage (pas au chargement du modulehttplui-même, contrairement àzip).json— nécessaire uniquement pour unbodyde type table encodé enapplication/json(par défaut) et pourresponse:json(). Chargée à la demande viarequire("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.
| 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
- Pas d'accès au corps de la réponse comme octets bruts en mémoire — le seul chemin prévu pour du
contenu binaire volumineux est
tofile. - Pas de raccourci asynchrone au niveau client (
client:getasyncet consorts n'existent pas). - Un
tofilecombiné à:sendasync()bufférise le corps entier en mémoire avant écriture (pas de streaming réel dans ce cas précis — voir « API asynchrone »). - Pas de cookies persistants entre requêtes au-delà du comportement par défaut du client HTTP sous-jacent.
- Pas de HTTP/2 push, WebSocket, SSE, certificats client configurables, ni cache de réponses.
- Annuler une requête asynchrone en cours n'est pas garanti annuler l'appel réseau sous-jacent.