Skip to content

HTTP Client

Make HTTP requests to external services. Supports all HTTP methods, headers, query parameters, form data, file uploads, streaming responses, and concurrent batch requests.

local http_client = require("http_client")

All methods share the same signature: method(url, options?) returning Response, error.

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) -- response body
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"})
})
local resp, err = http_client.put("https://api.example.com/users/123", {
headers = {["Content-Type"] = "application/json"},
body = json.encode({name = "Alice Smith"})
})
local resp, err = http_client.patch("https://api.example.com/users/123", {
body = json.encode({status = "active"})
})
local resp, err = http_client.delete("https://api.example.com/users/123", {
headers = {["Authorization"] = "Bearer " .. token}
})

Returns headers only, no body.

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"}
})
ParameterTypeDescription
methodstringHTTP method
urlstringRequest URL
optionstableRequest options (optional)
FieldTypeDescription
headerstableRequest headers {["Name"] = "value"}
bodystringRequest body
querytableQuery parameters {key = "value"}
formtableForm data (sets Content-Type automatically)
filestableFile uploads (array of file definitions)
cookiestableRequest cookies {name = "value"}
authtableBasic auth {user = "name", pass = "secret"}
timeoutnumber/stringTimeout: number in seconds, or string like "30s", "1m"
streambooleanStream response body instead of buffering
max_response_bodynumberMax response size in bytes (0 = default)
unix_socketstringConnect via Unix socket path
tlstablePer-request TLS configuration (see TLS Options)
overlay_networkstringRoute through a network overlay — registry ID of a network.socks5 / network.tailscale / network.i2p entry
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"
}
})
-- Or use 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", -- form field name
filename = "report.pdf", -- original filename
content = pdf_data, -- file content
content_type = "application/pdf"
}
}
})
File FieldTypeRequiredDescription
namestringyesForm field name
filenamestringnoOriginal filename
contentstringyes*File content
readeruserdatayes*Alternative: io.Reader for content
content_typestringnoCurrently ignored: each uploaded part is always sent with Content-Type: application/octet-stream regardless of this field

*Either content or reader is required.

-- Number: seconds
local resp, err = http_client.get(url, {timeout = 30})
-- String: Go duration format
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"})

Configure per-request TLS settings for mTLS (mutual TLS) and custom CA certificates.

FieldTypeDescription
certstringClient certificate in PEM format
keystringClient private key in PEM format
castringCustom CA certificate in PEM format
server_namestringServer name for SNI verification
insecure_skip_verifybooleanSkip TLS certificate verification

Both cert and key must be provided together for mTLS. The ca field overrides the system certificate pool with a custom CA.

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,
}
})
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",
}
})

Skip TLS verification for development environments. Requires the http_client.insecure_tls security permission.

local resp, err = http_client.get("https://localhost:8443/api", {
tls = {
insecure_skip_verify = true,
}
})
FieldTypeDescription
status_codenumberHTTP status code
bodystringResponse body (if not streaming)
body_sizenumberBody size in bytes (-1 if streaming)
headerstableResponse headers
cookiestableResponse cookies
urlstringFinal URL (after redirects)
streamStreamStream object (if 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

For large responses, use streaming to avoid loading entire body into memory.

local resp, err = http_client.get("https://cdn.example.com/large-file.zip", {
stream = true
})
if err then
return nil, err
end
-- Process in chunks
while true do
local chunk, err = resp.stream:read(65536)
if err or not chunk then break end
-- process chunk
end
resp.stream:close()
Stream MethodReturnsDescription
read(n?)string, errorRead up to n bytes (default: implementation buffer)
close()boolean, errorClose the stream

resp.stream is a full stream object — seek, stat, and scanner are also available.

Execute multiple requests concurrently.

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
-- All succeeded
for i, resp in ipairs(responses) do
print("Response " .. i .. ":", resp.status_code)
end
end
ParameterTypeDescription
requeststableArray of {method, url, options?}

Returns: responses, errors - arrays indexed by request position

Notes:

  • Requests execute concurrently
  • Streaming (stream = true) is not supported in batch
  • Result arrays match request order (1-indexed)
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 requests are subject to security policy evaluation.

ActionResourceDescription
http_client.requestURLAllow/deny requests to specific URLs
http_client.unix_socketSocket pathAllow/deny Unix socket connections
http_client.private_ipIP addressAllow/deny access to private IP ranges
http_client.insecure_tlsURLAllow/deny insecure TLS (skip verification)
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

Private IP ranges (10.x, 192.168.x, 172.16-31.x, localhost) are blocked by default. Access requires the http_client.private_ip permission.

local resp, err = http_client.get("http://192.168.1.1/admin")
-- Error: not allowed: private IP 192.168.1.1

See Security Model for policy configuration.

ConditionKindRetryable
Security policy deniederrors.PERMISSION_DENIEDno
Private IP blockederrors.PERMISSION_DENIEDno
Unix socket deniederrors.PERMISSION_DENIEDno
Insecure TLS deniederrors.PERMISSION_DENIEDno
Invalid URL or optionserrors.INVALIDno
No contexterrors.INTERNALno
Network failureerrors.INTERNALyes
Timeouterrors.INTERNALyes
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

See Error Handling for working with errors.