HTTP-клиент
Выполнение HTTP-запросов к внешним сервисам. Поддержка всех HTTP-методов, заголовков, query-параметров, данных форм, загрузки файлов, потоковых ответов и конкурентных пакетных запросов.
Загрузка
local http_client = require("http_client")
HTTP-методы
Все методы имеют одинаковую сигнатуру: method(url, options?), возвращают Response, error.
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) -- тело ответа
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"})
})
PUT-запрос
local resp, err = http_client.put("https://api.example.com/users/123", {
headers = {["Content-Type"] = "application/json"},
body = json.encode({name = "Alice Smith"})
})
PATCH-запрос
local resp, err = http_client.patch("https://api.example.com/users/123", {
body = json.encode({status = "active"})
})
DELETE-запрос
local resp, err = http_client.delete("https://api.example.com/users/123", {
headers = {["Authorization"] = "Bearer " .. token}
})
HEAD-запрос
Возвращает только заголовки, без тела.
local resp, err = http_client.head("https://cdn.example.com/file.zip")
local size = resp.headers["Content-Length"]
Произвольный метод
local resp, err = http_client.request("PROPFIND", "https://dav.example.com/folder", {
headers = {["Depth"] = "1"}
})
| Параметр | Тип | Описание |
|---|---|---|
method |
string | HTTP-метод |
url |
string | URL запроса |
options |
table | Опции запроса (опционально) |
Опции запроса
| Поле | Тип | Описание |
|---|---|---|
headers |
table | Заголовки запроса {["Name"] = "value"} |
body |
string | Тело запроса |
query |
table | Query-параметры {key = "value"} |
form |
table | Данные формы (Content-Type устанавливается автоматически) |
files |
table | Загрузка файлов (массив определений файлов) |
cookies |
table | Cookies запроса {name = "value"} |
auth |
table | Basic-авторизация {user = "name", pass = "secret"} |
timeout |
number/string | Таймаут: число в секундах или строка типа "30s", "1m" |
stream |
boolean | Потоковое получение тела вместо буферизации |
max_response_body |
number | Макс. размер ответа в байтах (0 = по умолчанию) |
unix_socket |
string | Подключение через Unix-сокет |
tls |
table | Настройки TLS для запроса (см. Параметры TLS) |
overlay_network |
string | Маршрутизировать через сетевой оверлей — registry ID записи network.socks5 / network.tailscale / network.i2p |
Query-параметры
local resp, err = http_client.get("https://api.example.com/search", {
query = {
q = "lua programming",
page = "1",
limit = "20"
}
})
Заголовки и авторизация
local resp, err = http_client.get("https://api.example.com/data", {
headers = {
["Authorization"] = "Bearer " .. token,
["Accept"] = "application/json"
}
})
-- Или через basic auth
local resp, err = http_client.get("https://api.example.com/data", {
auth = {user = "admin", pass = "secret"}
})
Данные формы
local resp, err = http_client.post("https://api.example.com/login", {
form = {
username = "alice",
password = "secret123"
}
})
Загрузка файлов
local resp, err = http_client.post("https://api.example.com/upload", {
form = {title = "My Document"},
files = {
{
name = "attachment", -- имя поля формы
filename = "report.pdf", -- исходное имя файла
content = pdf_data, -- содержимое файла
content_type = "application/pdf"
}
}
})
| Поле файла | Тип | Обязательно | Описание |
|---|---|---|---|
name |
string | да | Имя поля формы |
filename |
string | нет | Исходное имя файла |
content |
string | да* | Содержимое файла |
reader |
userdata | да* | Альтернатива: io.Reader для содержимого |
content_type |
string | нет | Сейчас игнорируется: каждая загружаемая часть всегда отправляется с Content-Type: application/octet-stream независимо от этого поля |
*Требуется либо content, либо reader.
Таймаут
-- Число: секунды
local resp, err = http_client.get(url, {timeout = 30})
-- Строка: формат Go duration
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"})
Параметры TLS
Настройка TLS для отдельных запросов: mTLS (взаимный TLS) и пользовательские CA-сертификаты.
| Поле | Тип | Описание |
|---|---|---|
cert |
string | Клиентский сертификат в формате PEM |
key |
string | Закрытый ключ клиента в формате PEM |
ca |
string | Пользовательский CA-сертификат в формате PEM |
server_name |
string | Имя сервера для SNI-верификации |
insecure_skip_verify |
boolean | Пропустить проверку TLS-сертификата |
Для mTLS необходимо указать оба поля cert и key. Поле ca заменяет системный пул сертификатов пользовательским CA.
Аутентификация 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
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",
}
})
Пропуск проверки TLS
Пропуск TLS-верификации для сред разработки. Требует разрешение безопасности http_client.insecure_tls.
local resp, err = http_client.get("https://localhost:8443/api", {
tls = {
insecure_skip_verify = true,
}
})
Объект ответа
| Поле | Тип | Описание |
|---|---|---|
status_code |
number | HTTP-код статуса |
body |
string | Тело ответа (если не потоковый) |
body_size |
number | Размер тела в байтах (-1 если потоковый) |
headers |
table | Заголовки ответа |
cookies |
table | Cookies ответа |
url |
string | Финальный URL (после редиректов) |
stream |
Stream | Объект потока (если 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
Потоковые ответы
Для больших ответов используйте потоковый режим, чтобы не загружать всё тело в память.
local resp, err = http_client.get("https://cdn.example.com/large-file.zip", {
stream = true
})
if err then
return nil, err
end
-- Обработка порциями
while true do
local chunk, err = resp.stream:read(65536)
if err or not chunk then break end
-- обработка chunk
end
resp.stream:close()
| Метод потока | Возвращает | Описание |
|---|---|---|
read(n?) |
string, error | Читает до n байт (по умолчанию: буфер реализации) |
close() |
boolean, error | Закрывает поток |
resp.stream — это полноценный объект stream: seek, stat и scanner также доступны.
Пакетные запросы
Выполнение нескольких запросов конкурентно.
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("Request " .. i .. " failed:", err)
end
end
else
-- Все успешны
for i, resp in ipairs(responses) do
print("Response " .. i .. ":", resp.status_code)
end
end
| Параметр | Тип | Описание |
|---|---|---|
requests |
table | Массив {method, url, options?} |
Возвращает: responses, errors — массивы, индексированные по позиции запроса
Замечания:
- Запросы выполняются конкурентно
- Потоковый режим (
stream = true) не поддерживается в пакетном режиме - Массивы результатов соответствуют порядку запросов (индексация с 1)
URL-кодирование
Кодирование
local encoded = http_client.encode_uri("hello world")
-- "hello+world"
local url = "https://api.example.com/search?q=" .. http_client.encode_uri(query)
Декодирование
local decoded, err = http_client.decode_uri("hello+world")
-- "hello world"
Разрешения
HTTP-запросы подчиняются вычислению политики безопасности.
Действия безопасности
| Действие | Ресурс | Описание |
|---|---|---|
http_client.request |
URL | Разрешить/запретить запросы к конкретным URL |
http_client.unix_socket |
Путь сокета | Разрешить/запретить подключения через Unix-сокет |
http_client.private_ip |
IP-адрес | Разрешить/запретить доступ к приватным IP-диапазонам |
http_client.insecure_tls |
URL | Разрешить/запретить небезопасный TLS (пропуск верификации) |
network.select |
ID записи сети | Разрешить/запретить маршрутизацию через overlay_network, указанный в запросе |
Проверка доступа
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
Защита от SSRF
Непубличные IP-диапазоны заблокированы по умолчанию. Доступ требует разрешения http_client.private_ip на адресе:
- loopback, приватные (10.x, 172.16-31.x, 192.168.x), link-local unicast и multicast, а также неуказанный адрес
- carrier-grade NAT
100.64.0.0/10,192.0.0.0/24, multicast224.0.0.0/4, зарезервированный240.0.0.0/4 - диапазоны для документации и бенчмарков
192.0.2.0/24,198.18.0.0/15,198.51.100.0/24,203.0.113.0/24,2001:db8::/32 - IPv6-multicast
ff00::/8
local resp, err = http_client.get("http://192.168.1.1/admin")
-- Error: not allowed: private IP 192.168.1.1
Проверка выполняется в момент подключения, а не по строке URL, и охватывает каждый адрес, в который разрешается хост. Имя хоста, разрешающееся в несколько адресов, проверяется адрес за адресом: запрещённый адрес пропускается и пробуется следующий, а запрос падает, только когда все кандидаты запрещены или недостижимы. Поэтому публичное имя хоста, разрешающееся в приватный адрес, блокируется ровно так же, как литерал приватного IP.
Редиректы
Выполняется до девяти редиректов; десятый падает с stopped after 10 redirects — счётчик включает исходный запрос.
Каждый переход авторизуется отдельно. Перед следованием за редиректом клиент вычисляет http_client.request для целевого URL и применяет к нему проверку приватных IP, поэтому разрешённый URL нельзя использовать, чтобы редиректом добраться до запрещённого. Переход, не прошедший любую из проверок, прерывает запрос.
См. Модель безопасности для настройки политик.
Ошибки
| Условие | Kind | Повторяемо |
|---|---|---|
| Запрещено политикой безопасности | errors.PERMISSION_DENIED |
нет |
| Приватный IP заблокирован | errors.PERMISSION_DENIED |
нет |
| Unix-сокет запрещён | errors.PERMISSION_DENIED |
нет |
| Небезопасный TLS запрещён | errors.PERMISSION_DENIED |
нет |
| Некорректный URL или опции | errors.INVALID |
нет |
| Нет контекста | errors.INTERNAL |
нет |
| Сетевая ошибка | errors.INTERNAL |
да |
| Таймаут | errors.INTERNAL |
да |
local resp, err = http_client.get(url)
if err then
if errors.is(err, errors.PERMISSION_DENIED) then
print("Access denied:", err:message())
elseif err:retryable() then
print("Temporary error:", err:message())
end
return nil, err
end
См. Обработка ошибок для работы с ошибками.