HTTP Client
HTTP Client
Section titled “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.
Loading
Section titled “Loading”local http_client = require("http_client")HTTP Methods
Section titled “HTTP Methods”All methods share the same signature: method(url, options?) returning Response, error.
GET Request
Section titled “GET Request”local resp, err = http_client.get("https://api.example.com/users")if err then return nil, errend
print(resp.status_code) -- 200print(resp.body) -- response bodyPOST Request
Section titled “POST Request”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 Request
Section titled “PUT Request”local resp, err = http_client.put("https://api.example.com/users/123", { headers = {["Content-Type"] = "application/json"}, body = json.encode({name = "Alice Smith"})})PATCH Request
Section titled “PATCH Request”local resp, err = http_client.patch("https://api.example.com/users/123", { body = json.encode({status = "active"})})DELETE Request
Section titled “DELETE Request”local resp, err = http_client.delete("https://api.example.com/users/123", { headers = {["Authorization"] = "Bearer " .. token}})HEAD Request
Section titled “HEAD Request”Returns headers only, no body.
local resp, err = http_client.head("https://cdn.example.com/file.zip")local size = resp.headers["Content-Length"]Custom Method
Section titled “Custom Method”local resp, err = http_client.request("PROPFIND", "https://dav.example.com/folder", { headers = {["Depth"] = "1"}})| Parameter | Type | Description |
|---|---|---|
method | string | HTTP method |
url | string | Request URL |
options | table | Request options (optional) |
Request Options
Section titled “Request Options”| Field | Type | Description |
|---|---|---|
headers | table | Request headers {["Name"] = "value"} |
body | string | Request body |
query | table | Query parameters {key = "value"} |
form | table | Form data (sets Content-Type automatically) |
files | table | File uploads (array of file definitions) |
cookies | table | Request cookies {name = "value"} |
auth | table | Basic auth {user = "name", pass = "secret"} |
timeout | number/string | Timeout: number in seconds, or string like "30s", "1m" |
stream | boolean | Stream response body instead of buffering |
max_response_body | number | Max response size in bytes (0 = default) |
unix_socket | string | Connect via Unix socket path |
tls | table | Per-request TLS configuration (see TLS Options) |
overlay_network | string | Route through a network overlay — registry ID of a network.socks5 / network.tailscale / network.i2p entry |
Query Parameters
Section titled “Query Parameters”local resp, err = http_client.get("https://api.example.com/search", { query = { q = "lua programming", page = "1", limit = "20" }})Headers and Authentication
Section titled “Headers and Authentication”local resp, err = http_client.get("https://api.example.com/data", { headers = { ["Authorization"] = "Bearer " .. token, ["Accept"] = "application/json" }})
-- Or use basic authlocal resp, err = http_client.get("https://api.example.com/data", { auth = {user = "admin", pass = "secret"}})Form Data
Section titled “Form Data”local resp, err = http_client.post("https://api.example.com/login", { form = { username = "alice", password = "secret123" }})File Upload
Section titled “File Upload”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 Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Form field name |
filename | string | no | Original filename |
content | string | yes* | File content |
reader | userdata | yes* | Alternative: io.Reader for content |
content_type | string | no | Currently ignored: each uploaded part is always sent with Content-Type: application/octet-stream regardless of this field |
*Either content or reader is required.
Timeout
Section titled “Timeout”-- Number: secondslocal resp, err = http_client.get(url, {timeout = 30})
-- String: Go duration formatlocal 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 Options
Section titled “TLS Options”Configure per-request TLS settings for mTLS (mutual TLS) and custom CA certificates.
| Field | Type | Description |
|---|---|---|
cert | string | Client certificate in PEM format |
key | string | Client private key in PEM format |
ca | string | Custom CA certificate in PEM format |
server_name | string | Server name for SNI verification |
insecure_skip_verify | boolean | Skip 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.
mTLS Authentication
Section titled “mTLS Authentication”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, }})Custom CA
Section titled “Custom 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", }})Insecure Skip Verify
Section titled “Insecure Skip Verify”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, }})Response Object
Section titled “Response Object”| Field | Type | Description |
|---|---|---|
status_code | number | HTTP status code |
body | string | Response body (if not streaming) |
body_size | number | Body size in bytes (-1 if streaming) |
headers | table | Response headers |
cookies | table | Response cookies |
url | string | Final URL (after redirects) |
stream | Stream | Stream object (if stream = true) |
local resp, err = http_client.get("https://api.example.com/data")if err then return nil, errend
if resp.status_code == 200 then local data = json.decode(resp.body) print("Content-Type:", resp.headers["Content-Type"])endStreaming Responses
Section titled “Streaming Responses”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, errend
-- Process in chunkswhile true do local chunk, err = resp.stream:read(65536) if err or not chunk then break end -- process chunkendresp.stream:close()| Stream Method | Returns | Description |
|---|---|---|
read(n?) | string, error | Read up to n bytes (default: implementation buffer) |
close() | boolean, error | Close the stream |
resp.stream is a full stream object — seek, stat, and scanner are also available.
Batch Requests
Section titled “Batch Requests”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 endelse -- All succeeded for i, resp in ipairs(responses) do print("Response " .. i .. ":", resp.status_code) endend| Parameter | Type | Description |
|---|---|---|
requests | table | Array 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)
URL Encoding
Section titled “URL Encoding”Encode
Section titled “Encode”local encoded = http_client.encode_uri("hello world")-- "hello+world"
local url = "https://api.example.com/search?q=" .. http_client.encode_uri(query)Decode
Section titled “Decode”local decoded, err = http_client.decode_uri("hello+world")-- "hello world"Permissions
Section titled “Permissions”HTTP requests are subject to security policy evaluation.
Security Actions
Section titled “Security Actions”| Action | Resource | Description |
|---|---|---|
http_client.request | URL | Allow/deny requests to specific URLs |
http_client.unix_socket | Socket path | Allow/deny Unix socket connections |
http_client.private_ip | IP address | Allow/deny access to private IP ranges |
http_client.insecure_tls | URL | Allow/deny insecure TLS (skip verification) |
Checking Access
Section titled “Checking Access”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")endSSRF Protection
Section titled “SSRF Protection”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.1See Security Model for policy configuration.
Errors
Section titled “Errors”| Condition | Kind | Retryable |
|---|---|---|
| Security policy denied | errors.PERMISSION_DENIED | no |
| Private IP blocked | errors.PERMISSION_DENIED | no |
| Unix socket denied | errors.PERMISSION_DENIED | no |
| Insecure TLS denied | errors.PERMISSION_DENIED | no |
| Invalid URL or options | errors.INVALID | no |
| No context | errors.INTERNAL | no |
| Network failure | errors.INTERNAL | yes |
| Timeout | errors.INTERNAL | yes |
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, errendSee Error Handling for working with errors.