Cliente HTTP
Realizar solicitudes HTTP a servicios externos. Soporta todos los metodos HTTP, cabeceras, parametros de consulta, datos de formulario, carga de archivos, respuestas en streaming y solicitudes por lotes concurrentes.
Carga
local http_client = require("http_client")
Metodos HTTP
Todos los metodos comparten la misma firma: method(url, options?) devolviendo Response, error.
Solicitud GET
local resp, err = http_client.get("https://api.example.com/users")
if err then
return nil, err
end
print(resp.status_code) -- 200
print(resp.body) -- cuerpo de respuesta
Solicitud POST
local resp, err = http_client.post("https://api.example.com/users", {
headers = {["Content-Type"] = "application/json"},
body = json.encode({name = "Alice", email = "alice@example.com"})
})
Solicitud PUT
local resp, err = http_client.put("https://api.example.com/users/123", {
headers = {["Content-Type"] = "application/json"},
body = json.encode({name = "Alice Smith"})
})
Solicitud PATCH
local resp, err = http_client.patch("https://api.example.com/users/123", {
body = json.encode({status = "active"})
})
Solicitud DELETE
local resp, err = http_client.delete("https://api.example.com/users/123", {
headers = {["Authorization"] = "Bearer " .. token}
})
Solicitud HEAD
Devuelve solo cabeceras, sin cuerpo.
local resp, err = http_client.head("https://cdn.example.com/file.zip")
local size = resp.headers["Content-Length"]
Método Personalizado
local resp, err = http_client.request("PROPFIND", "https://dav.example.com/folder", {
headers = {["Depth"] = "1"}
})
| Parámetro | Tipo | Descripción |
|---|---|---|
method |
string | Método HTTP |
url |
string | URL de solicitud |
options |
table | Opciones de solicitud (opcional) |
Opciones de Solicitud
| Campo | Tipo | Descripción |
|---|---|---|
headers |
table | Cabeceras de solicitud {["Name"] = "value"} |
body |
string | Cuerpo de solicitud |
query |
table | Parametros de consulta {key = "value"} |
form |
table | Datos de formulario (establece Content-Type automaticamente) |
files |
table | Carga de archivos (array de definiciones de archivo) |
cookies |
table | Cookies de solicitud {name = "value"} |
auth |
table | Autenticación basica {user = "name", pass = "secret"} |
timeout |
number/string | Timeout: número en segundos, o string como "30s", "1m" |
stream |
boolean | Transmitir cuerpo de respuesta en lugar de almacenar en buffer |
max_response_body |
number | Tamano maximo de respuesta en bytes (0 = predeterminado) |
unix_socket |
string | Conectar via ruta de socket Unix |
tls |
table | Configuracion TLS por solicitud (ver Opciones TLS) |
overlay_network |
string | Enrutar a través de una red overlay — ID de registro de una entrada network.socks5 / network.tailscale / network.i2p |
Parametros de Consulta
local resp, err = http_client.get("https://api.example.com/search", {
query = {
q = "lua programming",
page = "1",
limit = "20"
}
})
Cabeceras y Autenticación
local resp, err = http_client.get("https://api.example.com/data", {
headers = {
["Authorization"] = "Bearer " .. token,
["Accept"] = "application/json"
}
})
-- O usar autenticación basica
local resp, err = http_client.get("https://api.example.com/data", {
auth = {user = "admin", pass = "secret"}
})
Datos de Formulario
local resp, err = http_client.post("https://api.example.com/login", {
form = {
username = "alice",
password = "secret123"
}
})
Carga de Archivos
local resp, err = http_client.post("https://api.example.com/upload", {
form = {title = "My Document"},
files = {
{
name = "attachment", -- nombre de campo de formulario
filename = "report.pdf", -- nombre de archivo original
content = pdf_data, -- contenido del archivo
content_type = "application/pdf"
}
}
})
| Campo de Archivo | Tipo | Requerido | Descripción |
|---|---|---|---|
name |
string | si | Nombre de campo de formulario |
filename |
string | no | Nombre de archivo original |
content |
string | si* | Contenido del archivo |
reader |
userdata | si* | Alternativa: io.Reader para contenido |
content_type |
string | no | Actualmente ignorado: cada parte cargada se envía siempre con Content-Type: application/octet-stream sin importar este campo |
*Se requiere content o reader.
Timeout
-- Número: segundos
local resp, err = http_client.get(url, {timeout = 30})
-- String: formato de duración Go
local resp, err = http_client.get(url, {timeout = "30s"})
local resp, err = http_client.get(url, {timeout = "1m30s"})
local resp, err = http_client.get(url, {timeout = "1h"})
Opciones TLS
Configurar ajustes TLS por solicitud para mTLS (TLS mutuo) y certificados CA personalizados.
| Campo | Tipo | Descripcion |
|---|---|---|
cert |
string | Certificado de cliente en formato PEM |
key |
string | Clave privada del cliente en formato PEM |
ca |
string | Certificado CA personalizado en formato PEM |
server_name |
string | Nombre del servidor para verificacion SNI |
insecure_skip_verify |
boolean | Omitir verificacion de certificado TLS |
Tanto cert como key deben proporcionarse juntos para mTLS. El campo ca reemplaza el pool de certificados del sistema con un CA personalizado.
Autenticacion mTLS
local cert_pem = fs.read("/certs/client.crt")
local key_pem = fs.read("/certs/client.key")
local resp, err = http_client.get("https://secure.example.com/api", {
tls = {
cert = cert_pem,
key = key_pem,
}
})
CA Personalizado
local ca_pem = fs.read("/certs/internal-ca.crt")
local resp, err = http_client.get("https://internal.example.com/api", {
tls = {
ca = ca_pem,
server_name = "internal.example.com",
}
})
Omitir Verificacion Insegura
Omitir verificacion TLS para entornos de desarrollo. Requiere el permiso de seguridad http_client.insecure_tls.
local resp, err = http_client.get("https://localhost:8443/api", {
tls = {
insecure_skip_verify = true,
}
})
Objeto Response
| Campo | Tipo | Descripción |
|---|---|---|
status_code |
number | Código de estado HTTP |
body |
string | Cuerpo de respuesta (si no es streaming) |
body_size |
number | Tamano del cuerpo en bytes (-1 si es streaming) |
headers |
table | Cabeceras de respuesta |
cookies |
table | Cookies de respuesta |
url |
string | URL final (despues de redirecciones) |
stream |
Stream | Objeto Stream (si stream = true) |
local resp, err = http_client.get("https://api.example.com/data")
if err then
return nil, err
end
if resp.status_code == 200 then
local data = json.decode(resp.body)
print("Content-Type:", resp.headers["Content-Type"])
end
Respuestas en Streaming
Para respuestas grandes, use streaming para evitar cargar todo el cuerpo en memoria.
local resp, err = http_client.get("https://cdn.example.com/large-file.zip", {
stream = true
})
if err then
return nil, err
end
-- Procesar en fragmentos
while true do
local chunk, err = resp.stream:read(65536)
if err or not chunk then break end
-- procesar fragmento
end
resp.stream:close()
| Método de Stream | Devuelve | Descripción |
|---|---|---|
read(n?) |
string, error | Leer hasta n bytes (predeterminado: buffer de implementación) |
close() |
boolean, error | Cerrar el stream |
resp.stream es un objeto stream completo — seek, stat y scanner también están disponibles.
Solicitudes por Lotes
Ejecutar multiples solicitudes concurrentemente.
local responses, errors = http_client.request_batch({
{"GET", "https://api.example.com/users"},
{"GET", "https://api.example.com/products"},
{"POST", "https://api.example.com/log", {body = "event"}}
})
if errors then
for i, err in ipairs(errors) do
if err then
print("Solicitud " .. i .. " fallo:", err)
end
end
else
-- Todas exitosas
for i, resp in ipairs(responses) do
print("Respuesta " .. i .. ":", resp.status_code)
end
end
| Parámetro | Tipo | Descripción |
|---|---|---|
requests |
table | Array de {method, url, options?} |
Devuelve: responses, errors - arrays indexados por posicion de solicitud
Notas:
- Las solicitudes se ejecutan concurrentemente
- Streaming (
stream = true) no es soportado en lotes - Los arrays de resultado coinciden con el orden de solicitud (indexado desde 1)
Codificacion de URL
Codificar
local encoded = http_client.encode_uri("hello world")
-- "hello+world"
local url = "https://api.example.com/search?q=" .. http_client.encode_uri(query)
Decodificar
local decoded, err = http_client.decode_uri("hello+world")
-- "hello world"
Permisos
Las solicitudes HTTP estan sujetas a evaluacion de politica de seguridad.
Acciones de Seguridad
| Accion | Recurso | Descripción |
|---|---|---|
http_client.request |
URL | Permitir/denegar solicitudes a URLs especificas |
http_client.unix_socket |
Ruta de socket | Permitir/denegar conexiones de socket Unix |
http_client.private_ip |
Direccion IP | Permitir/denegar acceso a rangos de IP privados |
http_client.insecure_tls |
URL | Permitir/denegar TLS inseguro (omitir verificacion) |
network.select |
ID de entrada de red | Permitir/denegar el enrutamiento a traves del overlay_network indicado en la solicitud |
Verificar Acceso
local security = require("security")
if security.can("http_client.request", "https://api.example.com/users") then
local resp = http_client.get("https://api.example.com/users")
end
Proteccion SSRF
Los rangos de IP no publicos estan bloqueados por defecto. El acceso requiere el permiso http_client.private_ip sobre la direccion:
- loopback, privadas (10.x, 172.16-31.x, 192.168.x), unicast y multicast link-local, y la direccion no especificada
- NAT de nivel operador
100.64.0.0/10,192.0.0.0/24, multicast224.0.0.0/4, reservado240.0.0.0/4 - rangos de documentacion y benchmarking
192.0.2.0/24,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24,2001:db8::/32 - multicast IPv6
ff00::/8
local resp, err = http_client.get("http://192.168.1.1/admin")
-- Error: no permitido: IP privada 192.168.1.1
La comprobacion ocurre al momento del dial, no sobre la cadena de la URL, y cubre todas las direcciones a las que resuelve el host. Un nombre de host que resuelve a varias direcciones se comprueba direccion por direccion: una direccion denegada se omite y se prueba la siguiente, y la solicitud falla solo cuando todas las candidatas estan denegadas o inalcanzables. Por lo tanto, un nombre de host publico que resuelve a una direccion privada se bloquea exactamente igual que un literal de IP privada.
Redirecciones
Se siguen hasta nueve redirecciones; la decima falla con stopped after 10 redirects, un conteo que incluye la solicitud original.
Cada salto se autoriza por separado. Antes de seguir una redireccion, el cliente evalua http_client.request contra la URL destino y le aplica la comprobacion de IP privada, de modo que una URL permitida no puede usarse para alcanzar una denegada por redireccion. Un salto que falla cualquiera de las dos comprobaciones aborta la solicitud.
Consulte Modelo de Seguridad para configuración de politicas.
Errores
| Condición | Tipo | Reintentable |
|---|---|---|
| Politica de seguridad denegada | errors.PERMISSION_DENIED |
no |
| IP privada bloqueada | errors.PERMISSION_DENIED |
no |
| Socket Unix denegado | errors.PERMISSION_DENIED |
no |
| TLS inseguro denegado | errors.PERMISSION_DENIED |
no |
| URL u opciones invalidas | errors.INVALID |
no |
| Sin contexto | errors.INTERNAL |
no |
| Fallo de red | errors.INTERNAL |
si |
| Timeout | errors.INTERNAL |
si |
local resp, err = http_client.get(url)
if err then
if errors.is(err, errors.PERMISSION_DENIED) then
print("Acceso denegado:", err:message())
elseif err:retryable() then
print("Error temporal:", err:message())
end
return nil, err
end
Consulte Manejo de Errores para trabajar con errores.