Everything that leaves the machine lives under perch.net. Access is opt-in per host: each net.http:<host> or net.websocket:<host> entry in mod.json pins exactly one host, and the Workshop page shows every pinned host before a user subscribes.
{ "scopes": ["net.http:api.github.com", "net.websocket:stream.example.com"] }
Before pinning a host, check whether Perch already fetches what you need: Steam and Weather come with no network scope.
perch.net.http.get
perch.net.http.get(url: string, opts: http_options_t?, callback: http_callback_t): ()
| Argument | Meaning |
|---|---|
url | must start with https://, and the host must equal a pinned host exactly (no subdomains) |
opts | optional. ua sets the User-Agent (default perch-mod/<id>), timeout_ms the deadline. A function in this position is taken as the callback |
callback | called exactly once, on the main thread, with (res, err) |
Returns nothing. Anything but a function as the last argument raises.
perch.net.http.get("https://api.github.com/repos/hlpdev/perch", { ua = "my-mod/1.0" },
function(res: http_response_t?, err: string?)
if not res then perch.log("fetch failed: " .. (err or "")) return end
local repo = perch.json.decode(res.body)
if repo then stars:push({ count = tostring(repo.stargazers_count) }) end
end)
perch.net.http.post
perch.net.http.post(url: string, body: string, opts: http_options_t?, callback: http_callback_t): ()
Same rules as get. body must be a string; encode a table with perch.json.encode first. opts.content_type defaults to application/json.
perch.net.http.post("https://api.example.com/events", perch.json.encode({ kind = "deploy", ok = true }),
function(res: http_response_t?, err: string?)
perch.log(if res then "posted " .. res.status else "post failed: " .. (err or ""))
end)
The answer
| Outcome | res | err |
|---|---|---|
| status 200 to 399 | { status, body }, the body capped at 8 MiB | nil |
| status 400 and up | nil | "http <status>" |
| not https, or the host is not pinned | nil | "scope" |
| more than 30 requests in the rolling minute | nil | "rate" |
| no HTTP client in this build | nil | "unavailable" |
| DNS, TLS or timeout failure | nil | "" |
The refusals ("scope", "rate", "unavailable") call back synchronously, before get returns. If the mod stops while a request is in flight, the callback never runs.
Limits: 30 requests per rolling minute per mod across every host and both methods; 8 MiB body; headers limited to the User-Agent and, for POST, the Content-Type.
perch.net.websocket.connect
perch.net.websocket.connect(url: string, opts: websocket_options_t?): socket_t
A live connection for a feed that pushes. url is wss:// only, on a host pinned by net.websocket:<host>; opts.headers are sent with the handshake. Returns the socket at once; connecting happens in the background, and a failure to connect arrives as on_close, so there is one path for failure and shutdown alike. A refusal Perch can see immediately (a bad scheme, an unpinned host, a fifth socket) raises.
| Member | Meaning |
|---|---|
socket.on_message | signal_t<string>; one fire per complete message |
socket.on_close | signal_t<number, string>; fires (code, reason) exactly once |
socket:send(text) | send one text frame; false once the socket has closed |
socket:close() | ask for a clean close; on_close still fires |
socket:connected() | whether the socket is open |
At most 4 sockets per mod. Sockets close when the mod stops.
--!strict
local retry = timer.new(5000, { once = true })
local function open()
local s = perch.net.websocket.connect("wss://stream.example.com/ticks")
s.on_message:connect(function(text: string)
local tick = perch.json.decode(text)
if tick then w:push({ price = tick.price }) end
end)
s.on_close:connect(function(code: number, reason: string)
retry:start()
end)
end
retry.on_fire:connect(open)
perch.on_start:connect(open)
Types
export type http_response_t = { status: number, body: string }
export type http_options_t = { ua: string?, content_type: string?, timeout_ms: number? }
export type http_callback_t = (res: http_response_t?, err: string?) -> ()
The answer, the options and the callback shape of get and post.
export type websocket_options_t = { headers: { [string]: string }? }
export type socket_t = {
on_message: signal_t<string>,
on_close: signal_t<number, string>,
send: (self: socket_t, text: string) -> boolean,
close: (self: socket_t) -> (),
connected: (self: socket_t) -> boolean,
}
The handshake options and the socket handle.