DEMIENGINEDOCUMENTATION Everything is free
Networking & HTTP · Game systems

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, or nil, err when 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.json is a Lua table serialized as a JSON body (use body for 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
})
FieldMeaning
urlFull request URL, http:// or https://.
methodHTTP method string such as GET or POST.
headersTable of request header names and string values.
jsonLua table serialized as a JSON body. Mutually exclusive with body.
bodyRaw string body. Mutually exclusive with json.
timeout_msWhole-request timeout in milliseconds. Default 30000.
connect_timeout_msConnection setup timeout in milliseconds. Default 5000.
max_response_bytesCap on response size. Default 16 MiB; larger responses fail instead of exhausting memory.
max_header_bytesCap on response header size. Default 64 KiB.
ca_certificate_pemOptional 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: true when the request finished (successfully or not), false while it is in flight.
  • req:response() — returns the response table once done() is true, or nil if no response is available.
  • req:cancel() — requests cancellation. Returns nothing; done() becomes true with 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
FieldMeaning
okBoolean, true when transport succeeded and the status is 200–299.
statusInteger HTTP status code, or zero on transport failure.
bodyRaw response body as a string.
errorTransport error message, empty for completed HTTP responses including 4xx/5xx.
error_codeStable error code string for scripts to branch on.
jsonDecoded JSON value when the body parsed; JSON null uses Data.null. Empty or invalid JSON returns nil.
json_errorJSON parse or conversion error; empty for an empty or valid JSON body. It does not change ok.
headersTable 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_pem to 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 Location header 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 reports PROJECT_BUILD_PERMISSION_NETWORK_MISSING when 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. Poll events() or is_connected(); send(message) returns a boolean.
  • demi.network.tls.server — listen(port, certificate_path, private_key_path[, max_clients]) accepts inbound TLS connections. Poll events() for client IDs and use send(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 from n random 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) or connect_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.