silly.net.http
silly.net.http
The silly.net.http module provides server-side and client-side implementation for HTTP/1.1 and HTTP/2 protocols. Built on coroutines, it offers a clean asynchronous API that automatically handles protocol details (such as chunked transfer, persistent connections, protocol negotiation, etc.).
Module Import
local http = require "silly.net.http"Core Concepts
Protocol Support
- HTTP/1.1: Supports persistent connections, chunked transfer, pipelining
- HTTP/2: Supports multiplexing, server push, header compression
- Automatic Protocol Negotiation: Automatically selects protocol version via ALPN
- Client Connection Pooling: Both HTTP/1.1 and HTTP/2 connections are pooled and reused automatically
- Redirect Support:
http.getandhttp.postfollow redirects (301/302/303/307/308) automatically with loop detection
Stream Object
The HTTP module uses stream objects to represent HTTP connections:
- Server-side: Handler function receives a stream object to process requests
- Client-side: Request function returns a stream object for reading/writing
Server-side API
http.listen(conf)
Creates an HTTP server and starts listening.
- Parameters:
conf:table- Server configuration tableaddr:string(required) - Listen address, e.g.,"127.0.0.1:8080"or":8080"handler:function(required) - Request handler functionfunction(stream)tls:boolean|nil(optional) - Whentrue, listen with TLS (HTTPS / WSS). Must be set when you wantcertsto take effect; acerts-only config withouttls = truewill listen in plain HTTP.certs:table[]|nil(optional) - TLS certificate configuration (for HTTPS, requirestls = true)cert:string- PEM format certificatekey:string- PEM format private key
alpnprotos:string[]|nil(optional) - ALPN protocols to advertise (e.g.{"h2", "http/1.1"}). Only meaningful withtls = true.backlog:integer|nil(optional) - Listen queue size
- Returns:
- Success:
server- Server object - Failure:
nil, silly.errno- nil and transport-layer error
- Success:
- Example:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
local server, err = http.listen {
addr = "127.0.0.1:8080",
handler = function(stream)
-- Handle request
stream:respond(200, {
["content-type"] = "text/plain",
["content-length"] = #"Hello, World!",
})
stream:closewrite("Hello, World!")
end
}
if not server then
print("Server start failed:", err)
return
end
print("HTTP server listening on 127.0.0.1:8080")
end)server:close()
Closes the HTTP server.
- Parameters: None
- Returns: None
- Example:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
local server = http.listen {
addr = ":8080",
handler = function(stream)
stream:respond(200, {})
stream:closewrite("OK")
end
}
-- Close server later
server:close()
print("Server closed")
end)Server-side Stream API
The stream object received by the handler function provides the following properties and methods:
stream Properties
stream.method:string- HTTP method (GET, POST, PUT, etc.)stream.path:string- Request pathstream.query:table- Query parameters tablestream.header:table- Request headers table (lowercase keys)stream.version:string- Protocol version ("HTTP/1.1" or "HTTP/2")stream.remoteaddr:string- Client address
stream:respond(status, headers [, endstream])
Sends response status line and headers.
- Parameters:
status:integer- HTTP status code (200, 404, 500, etc.)headers:table- Response headers tableendstream:boolean|nil(optional) - Finish this response without a body or trailers; keep the connection reusable
- Returns:
- Success:
true - Failure:
false, string- false and error message
- Success:
- Example:
local http = require "silly.net.http"
-- Assuming inside a handler
local function handler(stream)
stream:respond(200, {
["content-type"] = "application/json",
["content-length"] = #'{"status":"ok"}',
})
stream:closewrite('{"status":"ok"}')
endstream:closewrite([body])
Sends the optional final response body and finishes writing the response.
Parameters:
body:string|nil(optional) - Response body content
Returns: None
Note: No more response data can be written; reading the request body remains available.
Example:
local http = require "silly.net.http"
-- Assuming inside a handler
local function handler(stream)
stream:respond(200, {
["content-type"] = "text/html",
["content-length"] = #"<h1>Hello</h1>",
})
stream:closewrite("<h1>Hello</h1>")
endstream:readall()
Reads the complete request body (asynchronous).
- Parameters: None
- Returns:
- Success:
string- Request body content - Failure:
nil, string- nil and error message
- Success:
- Asynchronous: Suspends the coroutine until the entire request body is read
- Example:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
http.listen {
addr = ":8080",
handler = function(stream)
if stream.method == "POST" then
local body, err = stream:readall()
if body then
print("Received POST data:", body)
stream:respond(200, {
["content-type"] = "text/plain",
["content-length"] = #"Received",
})
stream:closewrite("Received")
else
stream:respond(500, {})
stream:closewrite("Read error: " .. (err or "unknown"))
end
else
stream:respond(200, {})
stream:closewrite("OK")
end
end
}
end)stream:write(data)
Writes data to the response stream (HTTP/1.1 only).
- Parameters:
data:string- Data to write
- Returns:
- Success:
true - Failure:
false, string- false and error message
- Success:
- Note: HTTP/2 streams do not support this method; use
close(body)instead - Example:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
http.listen {
addr = ":8080",
handler = function(stream)
if stream.version == "HTTP/1.1" then
stream:respond(200, {
["content-type"] = "text/plain",
["transfer-encoding"] = "chunked",
})
stream:write("Chunk 1\n")
stream:write("Chunk 2\n")
stream:closewrite()
end
end
}
end)Client-side API
http.get(url [, headers])
Sends an HTTP GET request with automatic redirect following (asynchronous).
- Parameters:
url:string- Request URL (complete URL including protocol and host)headers:table|nil(optional) - Request headers table
- Returns:
- Success:
table- Response object containing:status:integer- HTTP status codeheader:table- Response headers tablebody:string- Response body
- Failure:
nil, string- nil and error message
- Success:
- Asynchronous: Suspends the coroutine until the complete response is received
- Example:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
local response, err = http.get("http://www.example.com")
if response then
print("Status:", response.status)
print("Body length:", #response.body)
print("Content-Type:", response.header["content-type"])
else
print("GET failed:", err)
end
end)http.post(url [, headers [, body]])
Sends an HTTP POST request with automatic redirect following (asynchronous).
- Parameters:
url:string- Request URLheaders:table|nil(optional) - Request headers tablebody:string|nil(optional) - Request body content
- Returns:
- Success:
table- Response object (same as GET) - Failure:
nil, string- nil and error message
- Success:
- Note: If
bodyis provided, thecontent-lengthheader is automatically set - Example:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
local json = require "silly.encoding.json"
task.fork(function()
local request_data = json.encode({
name = "Alice",
age = 30
})
local response, err = http.post(
"http://api.example.com/users",
{
["content-type"] = "application/json",
},
request_data
)
if response then
print("Status:", response.status)
print("Response:", response.body)
else
print("POST failed:", err)
end
end)http.request(method, url [, headers [, endstream [, timeout]]])
HTTP/2 client streams provide headeronly, indicating whether the response ends with its headers. Read it after waitresponse() succeeds.
Sends a custom HTTP request and returns a raw stream (no redirect handling).
- Parameters:
method:string- HTTP method (GET, POST, PUT, DELETE, etc.)url:string- Request URLheaders:table|nil(optional) - Request headers tableendstream:boolean|nil(optional) - Finish sending this request without a body or trailerstimeout:integer|nil(optional) - Maximum wait in milliseconds for HTTP/2 stream capacity or HTTP/1.1 100-continue; excludes connection setup and subsequent I/O
- Returns:
- Success:
stream- HTTP stream object - Failure:
nil, string- nil and error message
- Success:
- Note: The returned stream needs to be manually closed by calling
close(). Unlikehttp.get/http.post, this function does not follow redirects. - Example:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
local stream<close>, err = http.request(
"PUT",
"http://api.example.com/data",
{
["content-type"] = "text/plain",
["content-length"] = #"Updated data",
}
)
if not stream then
print("Request failed:", err)
return
end
stream:closewrite("Updated data")
local ok, wait_err = stream:waitresponse()
if ok then
print("Status:", stream.status)
end
end)Connection Pool Client API
The http.newclient function creates a dedicated HTTP client with connection pooling. This is useful for making multiple requests to the same host.
http.newclient([opts])
Creates a new HTTP client with connection pooling.
- Parameters:
opts:table|nil(optional) - Client configurationmax_idle_per_host:integer?- Maximum idle connections per host (default: 10)idle_timeout:integer?- Idle connection timeout in ms (default: 30000)alpnprotos:string[]?- ALPN protocols (default:{"http/1.1", "h2"})send_ping_timeout:integer(optional) - Receive inactivity before sending PING, in ms; nil or a nonpositive value disables probesping_timeout:integer(optional) - Timeout for one probe, in ms; nil or a nonpositive value uses 15000 when probes are enabled
- Returns:
client- HTTP client object - Example:
local http = require "silly.net.http"
local client = http.newclient({
max_idle_per_host = 5,
idle_timeout = 10000,
})client:get(url [, headers])
Sends a GET request with automatic redirect following (301/302/303/307/308).
- Parameters:
url:string- Request URLheaders:table|nil(optional) - Request headers table
- Returns:
- Success:
table- Response object withstatus,header,body - Failure:
nil, string- nil and error message
- Success:
- Redirect behavior:
- 301/302/303: Method changes to GET, body is dropped
- 307/308: Method and body are preserved
- Maximum 10 redirects; loops are detected and reported as errors
- Gzip responses are automatically decompressed
client:post(url [, headers [, body]])
Sends a POST request with automatic redirect following (301/302/303/307/308).
- Parameters:
url:string- Request URLheaders:table|nil(optional) - Request headers tablebody:string|nil(optional) - Request body content
- Returns: Same as
client:get - Redirect behavior: Same as
client:get - Note: If
bodyis provided, thecontent-lengthheader is automatically set
client:request(method, url [, headers [, endstream [, timeout]]])
Sends a request without redirect handling. Returns a raw stream for full control.
- Parameters:
method:string- HTTP methodurl:string- Request URLheaders:table|nil(optional) - Request headers tableendstream:boolean|nil(optional) - Finish sending this request without a body or trailerstimeout:integer|nil(optional) - Maximum wait in milliseconds for HTTP/2 stream capacity or HTTP/1.1 100-continue; excludes connection setup and subsequent I/O
- Returns:
- Success:
stream- HTTP stream object - Failure:
nil, string- nil and error message
- Success:
client:close()
Gracefully closes all pooled connections.
- Parameters: None
- Returns: None
Usage Examples
Example 1: Basic HTTP Server
A simple HTTP server that returns different content based on path:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
local server = http.listen {
addr = "127.0.0.1:8080",
handler = function(stream)
local path = stream.path
local response_body
if path == "/" then
response_body = "Welcome to the home page!"
elseif path == "/about" then
response_body = "This is the about page."
else
stream:respond(404, {
["content-type"] = "text/plain",
["content-length"] = #"Not Found",
})
stream:closewrite("Not Found")
return
end
stream:respond(200, {
["content-type"] = "text/plain",
["content-length"] = #response_body,
})
stream:closewrite(response_body)
end
}
print("Server listening on 127.0.0.1:8080")
end)Example 2: JSON API Server
A RESTful API server that handles JSON requests and responses:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
local json = require "silly.encoding.json"
task.fork(function()
local users = {
{id = 1, name = "Alice", age = 30},
{id = 2, name = "Bob", age = 25},
}
local server = http.listen {
addr = ":8080",
handler = function(stream)
if stream.method == "GET" and stream.path == "/api/users" then
-- Return user list
local response_body = json.encode(users)
stream:respond(200, {
["content-type"] = "application/json",
["content-length"] = #response_body,
})
stream:closewrite(response_body)
elseif stream.method == "POST" and stream.path == "/api/users" then
-- Create new user
local body, err = stream:readall()
if not body then
stream:respond(400, {})
stream:closewrite("Bad Request")
return
end
local user = json.decode(body)
if user and user.name and user.age then
user.id = #users + 1
table.insert(users, user)
local response_body = json.encode(user)
stream:respond(201, {
["content-type"] = "application/json",
["content-length"] = #response_body,
})
stream:closewrite(response_body)
else
stream:respond(400, {})
stream:closewrite("Invalid user data")
end
else
stream:respond(404, {})
stream:closewrite("Not Found")
end
end
}
print("JSON API server running on port 8080")
end)Example 3: Query Parameter Processing
Parsing and using URL query parameters:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
local server = http.listen {
addr = ":8080",
handler = function(stream)
local name = stream.query["name"] or "Guest"
local count = tonumber(stream.query["count"]) or 1
local response_body = string.format(
"Hello, %s! Count: %d",
name,
count
)
stream:respond(200, {
["content-type"] = "text/plain",
["content-length"] = #response_body,
})
stream:closewrite(response_body)
end
}
print("Server listening on port 8080")
print("Try: http://localhost:8080?name=Alice&count=5")
end)Example 4: HTTPS Server
Creating an HTTPS server with TLS certificates:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
local cert_pem = [[-----BEGIN CERTIFICATE-----
MIIDCTCCAfGgAwIBAgIUPc2faaWEjGh1RklF9XPAgYS5WSMwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTAwOTA5NDc1M1oXDTM1MTAw
NzA5NDc1M1owFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEApmUl+7J8zeWdOH6aiNwRSOcFePTxuAyYsAEewVtBCAEv
LVGxQtrsVvd6UosEd0aO/Qz3hvV32wYzI0ZzjGGfy0lCCx9YB05SyYY+KpDwe/os
Mf4RtBS/jN1dVX7TiRQ3KsngMFSXp2aC6IpI5ngF0PS/o2qbwkU19FCELE6G5WnA
fniUaf7XEwrhAkMAczJovqOu4BAhBColr7cQK7CQK6VNEhQBzM/N/hGmIniPbC7k
TjqyohWoLGPT+xQAe8WB39zbIHl+xEDoGAYaaI8I7TlcQWwCOIxdm+w67CQmC/Fy
GTX5fPoK96drushzwvAKphQrpQwT5MxTDvoE9xgbhQIDAQABo1MwUTAdBgNVHQ4E
FgQUsjX1LC+0rS4Ls5lcE8yg5P85LqQwHwYDVR0jBBgwFoAUsjX1LC+0rS4Ls5lc
E8yg5P85LqQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEADqDJ
HQxRjFPSxIk5EMrxkqxE30LoWKJeW9vqublQU/qHfMo7dVTwfsAvFpTJfL7Zhhqw
l20ijbQVxPtDwPB8alQ/ScP5VRqC2032KTi9CqUqTj+y58oDxgjnm06vr5d8Xkmm
nR2xhUecGkzFYlDoXo1w8XttMUefyHS6HWLXvu94V7Y/8YB4lBCEnwFnhgkYB9CG
RsleiOiZDsaHhnNQsnM+Xl1UJVxJlMStl+Av2rCTAj/LMHniXQ+9QKI/7pNDUeCL
qSdxZephYkeRF8C/i9R5G/gAL40kUFz0sgyXuv/kss3rrxsshKKTRbxnRm1k/J73
9ZiztVOeqpcxFxmf7Q==
-----END CERTIFICATE-----
]]
local key_pem = [[-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmZSX7snzN5Z04
fpqI3BFI5wV49PG4DJiwAR7BW0EIAS8tUbFC2uxW93pSiwR3Ro79DPeG9XfbBjMj
RnOMYZ/LSUILH1gHTlLJhj4qkPB7+iwx/hG0FL+M3V1VftOJFDcqyeAwVJenZoLo
ikjmeAXQ9L+japvCRTX0UIQsToblacB+eJRp/tcTCuECQwBzMmi+o67gECEEKiWv
txArsJArpU0SFAHMz83+EaYieI9sLuROOrKiFagsY9P7FAB7xYHf3NsgeX7EQOgY
BhpojwjtOVxBbAI4jF2b7DrsJCYL8XIZNfl8+gr3p2u6yHPC8AqmFCulDBPkzFMO
+gT3GBuFAgMBAAECggEAD5uyVetWuKuetVNu5IKcHnYJNeDoIacQ1YWtYF7SeVE/
HyWoFojZnYjGUSLYLuYP+J20RFUXQpTQzDDKGvN3XUbIaqmshLbsnhm5EB4baM29
Qo0+FOHTW//RxvjIF/Ys/JcGMBJnTV0Yz35VO0Ur6n9i0I3qAW2jk4DP/SX6kl9T
4iJj2Y+69y0bHjesfO71nCUUH6Ym2CHJRd6A4tCeYQr3U/CXOWggpUuPTXFWptt7
uSJjbTQgwUF5H83ih1CUdto1G5LPBUXVD5x2XZshgwZsL1au9kH2l/83BAHKK8io
LQ8FekLN6FLD83mvEwFPyrVhfipbeUz3bKrgEzvOmwKBgQDUbrAgRYCLxxpmguiN
0aPV85xc+VPL+dh865QHhJ0pH/f3fah/U7van/ayfG45aIA+DI7qohGzf03xFnO4
O51RHcRhnjDbXWY5l0ZpOIpvHLLCm8gqIAkX9bt7UyE+PxRSNvUt3kVFT3ZYnYCx
Wb1kiV1oRAzTf1l0X0qamFPqdwKBgQDIhV8OWTBrsuC0U3hmvNB+DPEHnyPWBHvI
+HMflas5gJiZ+3KvrS3vBOXFB3qfTD1LQwUPqeqY0Q41Svvsq2IQAkKedJDdMuPU
RoKaV/Qln85nmibscNcwVGQNUKTeSCJQ43ktrWT01UinamsSEOYTceMqwW10LDaF
Ff1MbKNs4wKBgQDMEPiIR7vQipdF2oNjmPt1z+tpNOnWjE/20KcHAdGna9pcmQ2A
IwPWZMwrcXTBGS34bT/tDXtLnwNUkWjglgPtpFa+H6R3ViWZNUSiV3pEeqEOaW/D
Z7rUlW5gbd8FWLtAryKfyWFpz4e0YLj7pWVWas6cFqLrmO5p6BBWqfYSyQKBgHyp
rjcVa+0JAHobircUm+pB0XeTkIv1rZ98FtaEDjdpo3XXxa1CVVRMDy03QRzYISMx
P2xFjvwCvHqVa5nv0r9xKEmq3oUmpk3KqFecZsUdXQ074QcOADqjvLAqetVWsz7m
rOeg7SrpjonGt1o7904Pd9OU/Z9D/YEv8pIY2GFRAoGASEf3+igRFSECUxLh9LZC
scAxCHh9sz15swDD/rdtEqLKGcxlu74YKkBnyQ/yWA4d/enPnvdP98ThXdXnX0X4
v1HSCliKZXW8cusnBRD2IOyxuIUV/qiMfARylMvlLBccgJR8+olH9f/yF2EFWhoy
125zQzr/ESlTL+5IWeNf2sM=
-----END PRIVATE KEY-----
]]
local server = http.listen {
addr = "127.0.0.1:8443",
certs = {
{
cert = cert_pem,
key = key_pem,
}
},
handler = function(stream)
stream:respond(200, {
["content-type"] = "text/plain",
["content-length"] = #"Hello, HTTPS!",
})
stream:closewrite("Hello, HTTPS!")
end
}
print("HTTPS server listening on 127.0.0.1:8443")
end)Example 5: HTTP Client Requests
Using the HTTP client API to send various requests:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
local json = require "silly.encoding.json"
task.fork(function()
-- GET request
local response, err = http.get("http://www.example.com")
if response then
print("GET Status:", response.status)
print("Body length:", #response.body)
end
-- GET request with headers
local response2, err = http.get("http://api.example.com/data", {
["user-agent"] = "Silly HTTP Client",
["accept"] = "application/json",
})
-- POST request
local post_data = json.encode({action = "create", value = 42})
local response3, err = http.post(
"http://api.example.com/action",
{["content-type"] = "application/json"},
post_data
)
if response3 then
print("POST Status:", response3.status)
local result = json.decode(response3.body)
print("Result:", result.message or "N/A")
end
end)Example 6: File Upload Server
An HTTP server that handles file uploads:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
local server = http.listen {
addr = ":8080",
handler = function(stream)
if stream.method == "POST" and stream.path == "/upload" then
local content_type = stream.header["content-type"] or ""
if content_type:find("multipart/form-data", 1, true) then
-- Read uploaded file content
local body, err = stream:readall()
if body then
print("Received upload, size:", #body)
-- In a real application, multipart data should be parsed
-- and the file saved here
stream:respond(200, {
["content-type"] = "text/plain",
["content-length"] = #"Upload successful",
})
stream:closewrite("Upload successful")
else
stream:respond(500, {})
stream:closewrite("Upload failed")
end
else
stream:respond(400, {})
stream:closewrite("Invalid content type")
end
else
stream:respond(404, {})
stream:closewrite("Not Found")
end
end
}
print("File upload server running on port 8080")
end)Important Notes
1. Coroutine Requirement
All HTTP APIs (server and client) must be called within a coroutine:
local silly = require "silly"
local http = require "silly.net.http"
-- Wrong: Cannot call in main thread
-- local response = http.get("http://example.com") -- Will fail
-- Correct: Call within a coroutine
task.fork(function()
local response = http.get("http://example.com")
-- ...
end)2. Content-Length Header
When sending responses, the correct content-length header should be set:
local http = require "silly.net.http"
-- Assuming inside a handler
local function handler(stream)
local body = "Response body"
-- Correct: Set content-length
stream:respond(200, {
["content-type"] = "text/plain",
["content-length"] = #body,
})
stream:closewrite(body)
end3. HTTP/2 vs HTTP/1.1
HTTP/2 and HTTP/1.1 have slightly different usage:
local http = require "silly.net.http"
-- Assuming inside a handler
local function handler(stream)
stream:respond(200, {["content-type"] = "text/plain"})
if stream.version == "HTTP/2" then
-- HTTP/2: Use close to send body
stream:closewrite("Hello, HTTP/2!")
else
-- HTTP/1.1: Can use write or close
stream:write("Hello, ")
stream:write("HTTP/1.1!")
stream:closewrite()
end
end4. Header Key Names
Request and response header keys are all lowercase:
local http = require "silly.net.http"
-- Assuming inside a handler
local function handler(stream)
-- Correct: Use lowercase keys
local content_type = stream.header["content-type"]
local user_agent = stream.header["user-agent"]
-- Wrong: Uppercase keys may not be found
-- local ct = stream.header["Content-Type"] -- May be nil
end5. Query Parameter Types
Query parameters are always strings and need manual conversion:
local http = require "silly.net.http"
-- Assuming inside a handler
local function handler(stream)
-- stream.query["page"] is the string "5"
local page = tonumber(stream.query["page"]) or 1
-- stream.query["debug"] is the string "true"
local debug = stream.query["debug"] == "true"
end6. Error Handling
Always check return values and handle possible errors:
local silly = require "silly"
local task = require "silly.task"
local http = require "silly.net.http"
task.fork(function()
local response, err = http.get("http://example.com")
if not response then
print("Request failed:", err)
return
end
if response.status ~= 200 then
print("HTTP error:", response.status)
return
end
-- Handle successful response
print("Body:", response.body)
end)Performance Recommendations
1. Connection Reuse
Both HTTP/1.1 and HTTP/2 support connection reuse, avoiding frequent connection creation:
local silly = require "silly"
local http = require "silly.net.http"
local task = require "silly.task"
task.fork(function()
-- HTTP module will automatically reuse connections (for the same host)
for i = 1, 10 do
local response = http.get("http://example.com/api/data?id=" .. i)
if response then
print("Request", i, "status:", response.status)
end
end
end)2. Concurrent Requests
Use coroutines to implement concurrent HTTP requests:
local silly = require "silly"
local http = require "silly.net.http"
local waitgroup = require "silly.sync.waitgroup"
local task = require "silly.task"
task.fork(function()
local wg = waitgroup.new()
local urls = {
"http://api1.example.com/data",
"http://api2.example.com/info",
"http://api3.example.com/status",
}
for i, url in ipairs(urls) do
wg:fork(function()
local response = http.get(url)
if response then
print("URL", i, "status:", response.status)
end
end)
end
wg:wait()
print("All requests completed")
end)3. Streaming Processing
For large files, consider using streaming processing instead of reading all at once:
local silly = require "silly"
local http = require "silly.net.http"
local task = require "silly.task"
task.fork(function()
http.listen {
addr = ":8080",
handler = function(stream)
if stream.method == "POST" then
-- For large files, chunked reading and processing can be used
-- Note: Current version of readall() reads everything at once
-- Consider future versions providing chunked read API
local body = stream:readall()
-- Process body...
stream:respond(200, {})
stream:closewrite("OK")
end
end
}
end)HTTP/2 PING keepalive
send_ping_timeout = 60000, ping_timeout = 15000 starts a PING after 60 seconds without received frames. If no matching ACK arrives within 15 seconds, the connection closes and blocked callers wake. Received frames postpone the next probe; sending data does not reset receive inactivity. Only a matching ACK completes an outstanding probe. Probes also run without active streams.
Active probes are disabled by default and enabled only on clients. Both endpoints acknowledge valid PING requests. PING bypasses DATA flow control and does not extend the HTTP connection pool's idle_timeout. See RFC 9113 §6.7.
Configure an established low-level client channel with ch:keepalive(send_ping_timeout, ping_timeout). Both arguments are optional integer milliseconds. A nil or nonpositive first argument disables probes. When probes are enabled, a nil or nonpositive second argument uses 15000 ms. Reconfiguration cancels the outstanding probe and replaces its timer while preserving elapsed receive inactivity. Keepalive uses one channel timer; stream read deadlines remain independent.
See Also
- silly - Core module
- silly.net.tcp - TCP protocol
- silly.net.tls - TLS/SSL encryption
- silly.encoding.json - JSON encoding/decoding
- silly.sync.waitgroup - Coroutine wait group