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, multicast 224.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

См. Обработка ошибок для работы с ошибками.