Skip to content

Http

A minimal blocking HTTP POST transport for small, low-frequency, out-of-band sends — opt-in telemetry, crash-report upload, a leaderboard submit. It is deliberately narrow: one method, JSON only, no headers, no GET (ADR 0009).

Http is not a general web client, and it is not the game transport — for connection-oriented gameplay traffic use Net (UDP via GameNetworkingSockets).

Import from the engine module:

import "engine" for Http

POST body to url with Content-Type: application/json. Blocking.

Returns: Map — always a Map with exactly three keys, never null:

  • "ok" (Bool) — true only when a response arrived with a 2xx status.
  • "status" (Num) — the HTTP status code. 0 means no response (transport error, bad URL, or the request was never sent).
  • "body" (String) — the response body; "" on any failure.

Parameters:

  • url (String) — a full URL including scheme: http:// or https://, host, optional port, and path. A URL without a scheme fails.
  • body (String) — the request body, sent as application/json. You build the JSON yourself; the engine does not serialize or validate it.
var res = Http.post("https://example.com/ingest", "{\"event\":\"level_end\"}")
if (res["ok"]) {
Logger.info("sent, HTTP %(res["status"])")
} else if (res["status"] == 0) {
Logger.warn("never reached the server") // offline, bad URL, or no networking
} else {
Logger.warn("server rejected it: HTTP %(res["status"])")
}

These are pinned by ADR 0009 and are not configurable from script or config:

LimitValue
MethodPOST only — no GET/PUT/DELETE
Content typeapplication/json, always
Request body cap1 MiB. A larger body is rejected before it is sent (ok=false, status=0)
Response body capnone — the whole response is read into memory
Connect timeout5 seconds
Read timeout10 seconds
Redirectsfollowed automatically
Schemehttp:// or https://; HTTPS via the vendored OpenSSL
Custom headersnot supported — see below

Passing a non-String for either argument is not an error: the value is silently treated as "", so a non-string url becomes an empty URL and the call fails with ok=false, status=0.

There is no auth-header parameter. Http.post cannot set Authorization, an API-key header, or any other header. The only place to put a credential is the URL query string:

Http.post("https://example.com/ingest?key=%(token)", payload)

Http.post links and runs on every platform, but only sends on networking-capable (desktop) builds. On a build compiled without networking (console stubs) it never crashes and never blocks — it returns ok=false, status=0, body="" immediately and logs a warning. Because there is no error key, treat a status=0 result as “did not send” and move on; never let a failed send block game progression.

A realistic shape: buffer events during play, flush once at a boundary, and drop the batch rather than stall the game if it fails.

import "engine" for Http, Engine, Logger
class Telemetry {
construct new(endpoint, token) {
_endpoint = endpoint
_token = token
_outbox = []
// A unique, non-deterministic install id. Engine.entropy() is NOT Random --
// Random is seed-deterministic (ADR 0002), so an id built from it collides
// across installs.
_installId = "%(Engine.entropy())"
}
// Cheap, non-blocking: safe to call during play.
record(name) {
if (_outbox.count < 128) _outbox.add("{\"e\":\"%(name)\"}")
}
// BLOCKING: call at a level boundary or on quit -- never from update(dt).
flush() {
if (_outbox.isEmpty) return
var payload = "{\"id\":\"%(_installId)\",\"events\":[%(_outbox.join(","))]}"
var res = Http.post("%(_endpoint)?key=%(_token)", payload)
if (res["ok"]) {
_outbox.clear()
} else {
// status 0 = never reached the server; the reason is only in the engine log.
Logger.warn("telemetry flush failed (HTTP %(res["status"])); dropping batch")
_outbox.clear() // bounded outbox: do not retry-grow forever
}
}
}

Keep the outbox bounded (as above) — an unbounded retry queue on a player who is offline for a whole session grows without limit, and every flush attempt costs another blocking 15-second worst case.


See Net for the real-time game transport, PlayFlow for dedicated-server matchmaking (which makes its own blocking HTTP calls), and Random for the seeded, deterministic RNG that Engine.entropy() is deliberately not.