Net API

The Net API provides:

  • HTTP(S) helpers

    • net_http_get(url, timeout_ms?)

    • net_http_post(url, content_type, body, timeout_ms?)

  • WebSocket client using WinHTTP’s WebSocket support

    • ws_connect(url, timeout_ms?) -> websocket userdata

    • WebSocket methods: send_text, send_binary, send_json, recv, poll, is_open, close

Supported URL schemes:

  • HTTP: http://, https://

  • WebSocket: ws://, wss:// (internally mapped to http:// / https:// for WinHTTP)

  • Works with hostnames and IP addresses, with or without custom ports.

⚠ These are low-level primitives intended for advanced users. Higher-level helpers/wrappers can be built on top, but are not part of this page.


HTTP API

net_http_get

ok, status_code, body = net_http_get(url, timeout_ms?)

Performs a synchronous HTTP/HTTPS GET.

Parameters

  • url : string

    • Full URL:

      • "https://example.com/api/test"

      • "http://127.0.0.1:8080/status"

  • timeout_ms : number (optional)

    • Total timeout (ms) applied to resolve, connect, send, receive.

    • 0 or nil = default WinHTTP timeouts.

Returns

  • ok : boolean

    • true if the HTTP request succeeded at the transport level (WinHTTP OK).

    • false if there was a network/protocol error.

  • status_code : integer

    • HTTP status code (200, 404, 500, ...)

    • 0 if the request failed before a response was received.

  • body : string

    • Response body as raw bytes (Lua string).

Example

local ok, status, body = net_http_get("https://httpbin.org/get", 5000)

if not ok then
    log("[HTTP] GET failed, status=" .. tostring(status))
    return
end

log("[HTTP] GET status=" .. status)
log("[HTTP] GET body=" .. body)

net_http_post

ok, status_code, body = net_http_post(url, content_type, body, timeout_ms?)

Performs a synchronous HTTP/HTTPS POST with a request body.

Parameters

  • url : string

    • Full URL, same as net_http_get.

  • content_type : string

    • MIME type, e.g.:

      • "application/json"

      • "application/x-www-form-urlencoded"

      • "text/plain; charset=utf-8"

  • body : string

    • Request body as a Lua string (raw bytes).

  • timeout_ms : number (optional)

    • Same semantics as net_http_get.

Returns

  • ok : boolean

  • status_code : integer

  • body : string (Same meaning as net_http_get.)

Example – POST JSON

local payload = '{"hello":"world","value":123}'

local ok, status, resp = net_http_post(
    "https://httpbin.org/post",
    "application/json",
    payload,
    5000
)

if not ok then
    log("[HTTP] POST failed, status=" .. tostring(status))
    return
end

log("[HTTP] POST status=" .. status)
log("[HTTP] POST body=" .. resp)

WebSocket API

The WebSocket API exposes:

  • A global constructor: ws_connect(url, timeout_ms?)

  • A userdata type net_ws with methods:

    • send_text, send_binary, send_json

    • recv, poll

    • is_open, close

Internally, the implementation uses WinHTTP:

  • ws:// → internally mapped to http://

  • wss:// → internally mapped to https://

  • A background receive thread continuously reads frames and pushes complete messages into a queue.

  • Lua sees messages via ws:recv() (blocking) or ws:poll() (non-blocking).

Global: ws_connect

ws, err = ws_connect(url, timeout_ms?)

Opens a client WebSocket connection.

Parameters

  • url : string

    • WebSocket URL, e.g.:

      • "wss://ws.postman-echo.com/raw"

      • "ws://127.0.0.1:9001/echo"

  • timeout_ms : number (optional)

    • WinHTTP timeouts (resolve, connect, send, receive).

Returns

  • On success:

    • ws : userdata (type net_ws)

      • A WebSocket object with methods described below.

  • On failure:

    • nil, "error string"

Example

local ws, err = ws_connect("wss://ws.postman-echo.com/raw", 5000)
if not ws then
    log("[WS] connect failed: " .. tostring(err))
    return
end

log("[WS] connected: " .. tostring(ws))  -- e.g. "websocket(open)"

WebSocket Object (net_ws)

Once connected, you have a userdata with metatable "net_ws", exposing these methods:

  • ws:send_text(message)

  • ws:send_binary(data)

  • ws:send_json(value)

  • ws:recv()

  • ws:poll()

  • ws:is_open()

  • ws:close(code?)

And metamethods:

  • __gc – automatic cleanup

  • __tostring"websocket(open)" or "websocket(closed)"


ws:send_text

ok = ws:send_text(message)

Sends a UTF-8 text message.

Parameters

  • message : string – Lua string, treated as UTF-8.

Returns

  • ok : booleantrue if the send call succeeded; false otherwise.

Example

local ok = ws:send_text("hello from PCX")
log("[WS] send_text ok = " .. tostring(ok))

ws:send_binary

ok = ws:send_binary(data)

Sends a binary WebSocket frame.

Parameters

  • data : string – raw bytes (Lua string).

⚠ Some public echo servers (e.g. Postman’s) close the connection when they receive binary frames. That’s a server behavior, not a bug in the API.

Returns

  • ok : boolean

Example

local bin = string.char(0,1,2,3,4,5)
local ok = ws:send_binary(bin)
log("[WS] send_binary ok = " .. tostring(ok))

ws:send_json

ok = ws:send_json(value)

Sends a JSON text message. Supports:

  • value = string: sent directly as UTF-8.

  • value = table: encoded via global Lua function json_encode.

Parameters

  • value : string | table

    • string – assumed to already be valid JSON.

    • table – will call json_encode(value) to produce JSON.

Requirements

  • For table:

    • A global function json_encode must exist:

      function json_encode(tbl) -> string
    • If json_encode is missing or throws, ws:send_json raises a Lua error.

Returns

  • ok : boolean

Example – string

local js = '{"type":"ping","time":123.45}'
ws:send_json(js)

Example – table

-- Assuming json_encode is registered globally in this environment.

ws:send_json({
    type = "ping",
    time = perf_time(),
    source = "pcx"
})

ws:recv (blocking)

msg, is_text = ws:recv()

Blocking receive that waits for a complete WebSocket message to be available in the queue.

  • Uses the internal message queue (filled by a background thread).

  • Does not block WinHTTP directly; it loops until:

    • A message is dequeued, or

    • The socket is closed and the queue is empty.

Returns

  • On message:

    • msg : string – message payload (text or binary).

    • is_text : boolean

      • true for text frames

      • false for binary frames

  • On closed and empty:

    • nil

⚠ This is a blocking loop with a small sleep (Sleep(1)). Best used in worker scripts or one-shot tests, not every frame in the UI thread.

Example

local msg, is_text = ws:recv()
if not msg then
    log("[WS] recv: connection closed or error")
else
    log("[WS] recv: kind=" .. (is_text and "text" or "binary")
        .. " len=" .. #msg)
end

ws:poll (non-blocking)

msg, is_text_or_closed = ws:poll()

Non-blocking receive from the internal message queue.

You get three possible outcomes:

  1. Message available

    • msg : string – message payload

    • is_text_or_closed : booleantrue = text, false = binary

  2. No message yet, still open

    • msg = nil

    • is_text_or_closed is nil

  3. Socket closed and queue empty

    • msg = nil

    • is_text_or_closed = false

This makes it easy to integrate in per-frame loops:

  • msg != nil → handle it

  • msg == nil and is_text_or_closed == nil → nothing yet

  • msg == nil and is_text_or_closed == false → closed

Example: per-frame polling

function on_frame()
    if not ws then return end

    local msg, flag = ws:poll()
    if msg then
        local kind = flag and "text" or "binary"
        log("[WS] poll: " .. kind .. " msg=" .. msg)
    elseif flag == false then
        log("[WS] poll: socket closed")
        ws = nil
    end
end

ws:is_open

is_open = ws:is_open()

Checks whether the socket is currently open.

  • Reflects the internal is_open flag updated by the background thread and close logic.

Returns

  • true if the socket is open.

  • false if it is closed or has encountered a fatal error.

Example

if ws and ws:is_open() then
    ws:send_text("still here")
else
    log("[WS] socket is closed")
end

ws:close

ws:close(code?)

Closes the WebSocket connection.

  • Signals the background receive thread to stop.

  • Sends a WebSocket close frame (best effort).

  • Waits for the thread to exit, then closes all WinHTTP handles.

Parameters

  • code : integer (optional)

    • WebSocket close status. Defaults to WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS.

Example

if ws then
    ws:close()  -- normal clean shutdown
    ws = nil
end

Metamethods

__gc

Called automatically when the Lua userdata is garbage collected.

  • Calls ws_close_internal, deletes the critical section, frees lua_ws_t.

  • You don’t call this directly; it’s tied to object lifetime.

__tostring

Used when you do:

tostring(ws)
  • Returns "websocket(open)" or "websocket(closed)".

Example:

log("WS object: " .. tostring(ws))

Summary

Global HTTP:

ok, status, body = net_http_get(url, timeout_ms?)
ok, status, body = net_http_post(url, content_type, body, timeout_ms?)

Global WebSocket:

ws, err = ws_connect(url, timeout_ms?)

WebSocket object (net_ws):

ws:send_text(message)         -- bool
ws:send_binary(data)          -- bool
ws:send_json(value)           -- bool (string or table with json_encode)
ws:recv()                     -- msg, is_text | nil  (blocking)
ws:poll()                     -- msg, is_text | nil | nil,false
ws:is_open()                  -- bool
ws:close(code?)               -- nil
tostring(ws)                  -- "websocket(open)" / "websocket(closed)"

Last updated