REST calls, HTTPS verification, and TLS sockets.
HTTP & TLS
Games talk to the outside world: fetching a news panel, posting a high score, connecting to a service over an encrypted channel. HTTP is the standard request/response protocol for those calls. TLS is the encryption layer that makes them safe on public networks. DemiEngine provides both as Lua services so gameplay code can call a web API without stopping the game, and can encrypt peer connections when the session requires it. This page explains those APIs from the ground up.
Why asynchronous HTTP
A web request can take hundreds of milliseconds — an eternity in a frame budget. The HTTP service is therefore asynchronous: you start a request, the game keeps running, and you poll for completion. That design keeps the frame loop responsive and makes timeouts explicit instead of hanging the game.
Starting a request
Import the HTTP service and start a request with get, post, or the general request form:
local Http = require("demi.network.http")
local req = Http.get("https://demiengine.de/api/news")
require("demi.network.http")— imports the HTTP service table.Http.get(url)— starts a GET request. Returns the request handle, ornil,errwhen the request could not even be started (bad URL, unsupported scheme, over the size caps).
local req = Http.post("https://demiengine.de/api/scores",
{ json = { name = "Skyforge", score = 1200 } })
Http.post(url, options)— starts a POST request.options.jsonis a Lua table serialized as a JSON body (usebodyfor a raw string instead — the two are exclusive).
The general form accepts every option at once:
local req, err = Http.request({
url = "https://demiengine.de/api/news",
method = "GET",
headers = { ["accept"] = "application/json" },
json = nil, -- or body = "raw text"; never both
timeout_ms = 30000,
connect_timeout_ms = 5000,
max_response_bytes = 16 * 1024 * 1024,
max_header_bytes = 64 * 1024,
ca_certificate_pem = trusted_ca_pem_text
})
| Field | Meaning |
|---|---|
url | Full request URL, http:// or https://. |
method | HTTP method string such as GET or POST. |
headers | Table of request header names and string values. |
json | Lua table serialized as a JSON body. Mutually exclusive with body. |
body | Raw string body. Mutually exclusive with json. |
timeout_ms | Whole-request timeout in milliseconds. Default 30000. |
connect_timeout_ms | Connection setup timeout in milliseconds. Default 5000. |
max_response_bytes | Cap on response size. Default 16 MiB; larger responses fail instead of exhausting memory. |
max_header_bytes | Cap on response header size. Default 64 KiB. |
ca_certificate_pem | Optional explicit PEM trust bundle for this request. A nonempty bundle replaces the default trust sources; it is not appended to them. Certificate and hostname verification stay enabled. |
Http.request(...) returns request, nil on success (a handle and no error), or nil, err when the request cannot start. A started request is not a finished one — check it every frame.
Waiting for the response
function Game:on_update(dt)
if not req then return end
if not req:done() then return end
local res = req:response()
end
req:done()— returns a boolean:truewhen the request finished (successfully or not),falsewhile it is in flight.req:response()— returns the response table oncedone()istrue, ornilif no response is available.req:cancel()— requests cancellation. Returns nothing;done()becomestruewith a cancellation error.
The response table:
local res = req:response()
if res.ok then
print(res.status) -- integer HTTP status
local data = res.json -- decoded JSON value, or nil
for name, values in pairs(res.headers) do
print(name, values[1]) -- header names are lowercase; values are arrays
end
else
print(res.status, res.error, res.error_code)
print(res.body) -- raw response text, if any
end
| Field | Meaning |
|---|---|
ok | Boolean, true when transport succeeded and the status is 200–299. |
status | Integer HTTP status code, or zero on transport failure. |
body | Raw response body as a string. |
error | Transport error message, empty for completed HTTP responses including 4xx/5xx. |
error_code | Stable error code string for scripts to branch on. |
json | Decoded JSON value when the body parsed; JSON null uses Data.null. Empty or invalid JSON returns nil. |
json_error | JSON parse or conversion error; empty for an empty or valid JSON body. It does not change ok. |
headers | Table of response headers: names lowercased, values arrays of strings. |
HTTPS, redirects, and Android
In a Task.fork worker, req:wait(timeout_ms) waits on a native completion notification and returns the same response fields as response(), followed by a nil error. Omit the timeout to wait indefinitely; zero checks without blocking. Explicit timeouts must be integers from 0 through 2,147,483,647 milliseconds. A wait timeout returns nil, "timeout"; worker cancellation returns nil, "cancelled". Neither cancels the request. Transport failures still return a response table: inspect ok, error and error_code. Main VM calls, including its coroutines and completed requests, return nil, error. Create and retain the handle in the worker; HTTP userdata cannot be transferred between VMs.
- HTTPS verifies the certificate chain and hostname. A mismatch fails the request. Omit
ca_certificate_pemto use platform trust, or supply the full bundle you intend to trust for this request. A supplied bundle replaces the platform trust sources. - 3xx redirects are not followed. The response arrives with its status and
Locationheader so your code can decide to retry — the engine will not silently change the destination of a request. - Android needs the INTERNET permission. For a project with explicit Android build settings, declare
android.permission.INTERNET; validation reportsPROJECT_BUILD_PERMISSION_NETWORK_MISSINGwhen a network service is used without it. See Android builds.
TLS client and server
TLS (Transport Layer Security) encrypts a byte stream and lets the client verify the server certificate and hostname. Use the TLS services for a custom byte-stream protocol.
demi.network.tls.client—connect(host, port, trusted_certificate_path[, server_name])starts an outbound TLS connection. Pollevents()oris_connected();send(message)returns a boolean.demi.network.tls.server—listen(port, certificate_path, private_key_path[, max_clients])accepts inbound TLS connections. Pollevents()for client IDs and usesend(client_id, message).
The TLS bindings resolve certificate arguments as project paths to certificate and key files. This differs from HTTP's ca_certificate_pem, which takes PEM text. The client verifies the supplied trust chain and server name. The server presents its certificate; these APIs do not require client certificates.
Crypto helpers
Three small helpers cover the common secrets work of a networked game:
local crypto = require("demi.network.crypto")
local token = crypto.random_token(32) -- 64 hex characters from 32 random bytes
local salt = crypto.random_token(16)
local hash = crypto.password_hash("hunter2", salt) -- PBKDF2-SHA256 hash hex
local same = crypto.secure_equals(hash, hash) -- boolean
crypto.random_token(n)— returns a lowercase hex string generated fromnrandom bytes (clamped to 1–64); an empty string indicates failure. Use for session tokens and salts.crypto.password_hash(password, salt[, iterations])— derives a PBKDF2-SHA256 hash and returns its hex string, or an empty string on failure. Store the salt and iteration count alongside the hash; the function does not embed them in its result.crypto.secure_equals(a, b)— returns a boolean comparison that does not short-circuit on the first differing byte, so timing does not leak how much of a secret matched.
Signing material, certificates, and credentials must never enter source, command output, or packaged content.
Low-level transport
Under the session layer sits a small transport API in demi.network for building custom protocols:
host(port[, max_peers])— opens an ENet UDP host and returns a boolean.host_dtls(port, certificate_path, private_key_path[, max_peers])— opens an ENet host with DTLS and returns a boolean.connect(host, port)orconnect_dtls(host, port, trusted_certificate_path[, server_name])— starts a client connection and returns a boolean.send(message[, reliable, peer_id, channel])— queues a byte string and returns a boolean.events()— drains the transport event queue and returns an array of connect, data, and disconnect events.
Most games never need this layer: Sessions & contracts builds replication, validation, and ownership on top of it. Choose the low-level API only when the session model does not fit your protocol.