Networking
@lune/net covers four jobs: sending HTTP requests, serving them, talking over WebSockets, and
raw TCP. Every call that waits on the network yields the calling thread only, so other tasks keep
running.
local net = require("@lune/net")
local response = net.request("https://example.com")print(response.statusCode, response.statusMessage)Sending a request
Section titled “Sending a request”net.request takes a URL, or a table of parameters. Only url is required.
local net = require("@lune/net")local serde = require("@lune/serde")
local response = net.request({ url = "https://httpbingo.org/anything", method = "POST", query = { page = "1", tag = { "a", "b" } }, headers = { ["Content-Type"] = "application/json" }, body = serde.encode("json", { title = "Cool Pencil" }),})
if response.ok then local data = serde.decode("json", response.body) print(data)else print("request failed:", response.statusCode, response.statusMessage)end| Parameter | Meaning |
|---|---|
url |
The URL to request. Required. |
method |
"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS" or "PATCH". Default "GET". |
body |
A string or buffer. |
query |
Query parameters. Values are URL-encoded for you; a list of strings sends the key several times. |
headers |
Request headers. A value may be a string or a list of strings. |
options |
{ decompress = false } turns automatic decompression of the response body off. |
The response is a table:
| Field | Meaning |
|---|---|
ok |
true when the status code is in the range 200 to 299. |
statusCode |
The numeric status code. |
statusMessage |
The canonical message for the code, such as "Not Found". |
headers |
Response headers. Names are lowercase. |
body |
The body as a string, or an empty string. |
Redirects are followed. A compressed response is decompressed by default, and its content-encoding
header is removed; with options = { decompress = false } the body and the header are left as sent.
net has no JSON functions of its own. Encode request bodies and decode response bodies with
serde.encode("json", value) and serde.decode("json", text) from @lune/serde, as in the example
above. Decoding throws on a body that is not JSON, so guard it when the server may answer with an
error page:
local ok, data = pcall(serde.decode, "json" :: "json", response.body)URL encoding
Section titled “URL encoding”Values passed in query are encoded for you. For anything you put into a URL by hand, use
net.urlEncode and net.urlDecode.
local net = require("@lune/net")
local encoded = net.urlEncode("a value with spaces")print(encoded) --> a%20value%20with%20spacesprint(net.urlDecode(encoded)) --> a value with spacesBoth take an optional second argument, binary. Pass true when the string is binary data or is
not valid UTF-8.
Serving HTTP
Section titled “Serving HTTP”net.serve(port, handler) starts a server and returns at once. The handler is called for each
request and returns either a string, sent with status 200, or a response table.
local net = require("@lune/net")
local handle = net.serve(8080, function(request) if request.path == "/" then return "Hello from LuneBlox!" elseif request.method == "POST" and request.path == "/echo" then return { status = 200, headers = { ["Content-Type"] = "text/plain" }, body = request.body, } end return { status = 404, body = "Not Found" }end)
print("Listening on port 8080")The request table:
| Field | Meaning |
|---|---|
path |
The path requested, / when none was given. |
query |
Query parameters. A key given more than once arrives as a list of strings. |
method |
The HTTP method, always uppercase. |
headers |
Request headers. |
body |
The body as a string, or an empty string. |
The response table may set status (100 to 599), headers and body (a string or buffer). All
three are optional.
The script keeps running while the server is listening. Call handle:stop() to shut it down;
handle.stop(), as scripts written for earlier releases call it, works too. Calling stop a second
time throws.
handle:stop()A handler may yield. It can call net.request, fs.readFile or task.wait and other requests are
still served in the meantime.
Choosing the address
Section titled “Choosing the address”By default the server listens on the loopback interface only. To accept connections from other
machines, pass a config table with address next to the handler. The address is an IP, with or
without an http:// prefix.
net.serve(8080, { address = "http://0.0.0.0", handleRequest = function(request) return "Hello!" end,})A malformed address throws.
Serving WebSockets
Section titled “Serving WebSockets”Add handleWebSocket to the config table. A request that asks for a WebSocket upgrade is upgraded
automatically and passed to it, never to handleRequest. The handler receives one socket per
connection and runs in its own thread.
local net = require("@lune/net")
net.serve(8080, { handleRequest = function(request) return "This is the HTTP side." end, handleWebSocket = function(socket) repeat local message = socket:next() if message ~= nil then socket:send("Echo - " .. message) end until message == nil end,})The config table needs at least one of the two handlers. When handleRequest is left out, a plain
HTTP request is answered with 426 Upgrade Required.
WebSocket clients
Section titled “WebSocket clients”net.socket(url) connects to a ws:// or wss:// URL and returns the same kind of socket the
server handler receives. It throws when the server does not support WebSockets or the connection
fails.
local net = require("@lune/net")
local socket = net.socket("ws://127.0.0.1:8080")
socket:send("ping")print(socket:next()) --> Echo - ping
socket:close()print(socket.closeCode) --> 1000| Member | Meaning |
|---|---|
socket:send(message, asBinaryMessage) |
Sends a string or buffer. Pass true as the second argument to send a binary message. |
socket:next() |
Yields until the next message arrives and returns it. Returns nil once the socket is closed. |
socket:close(code) |
Closes the socket, with an optional close code. |
socket.closeCode |
nil while open. After closing, a code between 1000 and 4999; 1000 is a normal closure. |
A connection closed without a close code reports 1005 (no status) or 1006 (abnormal closure).
To read and write at the same time, read in a separate task:
local net = require("@lune/net")local task = require("@lune/task")
local socket = net.socket("ws://127.0.0.1:8080")
task.spawn(function() while true do local message = socket:next() if message == nil then break end print("received", message) endend)
socket:send("first")socket:send("second")net.tcp.connect(host, port, config) opens a plain TCP stream, for protocols that are not HTTP. For
HTTP, use net.request and net.serve.
local net = require("@lune/net")
local stream = net.tcp.connect("example.com", 80)stream:write("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")
local chunks = {}while true do local chunk = stream:read() if chunk == nil or #chunk == 0 then break end table.insert(chunks, chunk)endstream:close()
print(table.concat(chunks))- The third argument is
truefor a TLS connection, or a table:{ tls = true }, and optionallyttlfor the packets’ time to live. stream:read(size)yields until data is available and returns up tosizebytes. It returns nil when the stream is closed.stream:write(data)takes a string or buffer, and throws afterstream:close().stream.localIp,stream.localPort,stream.remoteIpandstream.remotePortdescribe the two ends of the connection.net.tcp.connectthrows when the connection fails.
Reference
Section titled “Reference”The full listing is on the net reference. JSON and compression are on the serde reference, and running requests concurrently is covered in the task scheduler guide.
