Appearance
http
Calls a service outside the world: a leaderboard site, a chat webhook, your own database, a payment API.
luau
local http = require("http")Not for saving a player's progress
players.store already saves per player, per experience, with no network in the way and no limits like the ones below. Reach for http when you need to talk to somebody else's service.
send
luau
http:send(request, onAnswer) -> true | nil, reasonluau
local http = require("http")
local json = require("json")
http:send({
url = "https://api.example.com/score",
method = "POST",
headers = { ["content-type"] = "application/json" },
body = json.encode({ name = "ana", points = 42 }),
}, function(res)
if res.ok then
print("saved")
else
print("failed: " .. (res.error or tostring(res.status)))
end
end)One verb. The method is a field, not a different function.
| field | |
|---|---|
url | required. https:// only, port 443 |
method | GET (default), POST, PUT, PATCH, DELETE |
headers | names are lower-cased; up to 16 |
body | up to 64 KB. A GET cannot carry one |
The callback is optional. Leave it out for a call whose answer you do not care about, like a notification.
The answer arrives later
This is the one thing to hold on to. The internet takes time, and the world cannot stop and wait for it — every script, the physics and every player standing in the region would freeze along with it. So send returns straight away and your function is called when the answer comes back.
Three consequences:
It is a new event. The callback gets a fresh instruction budget, and the count of things it may change in the world starts again at 8. It is not a continuation of the handler that called send.
The order is not promised. Two requests come back in whatever order the network settles them.
luau
-- Wrong: `total` may be read before either answer arrived.
local total = 0
http:send({ url = a }, function(res) total = total + 1 end)
http:send({ url = b }, function(res) total = total + 1 end)
print(total) -- 0
-- Right: count inside, and act when the last one lands.
local left = 2
local function done()
left = left - 1
if left == 0 then print("both are in") end
end
http:send({ url = a }, done)
http:send({ url = b }, done)The world moved on. The callback reads the world as it is now — a second is a long time in a region. The player who touched the button may have walked away; check before acting on them.
What comes back
luau
http:send({ url = "https://api.example.com/who" }, function(res)
print(res.ok) -- true when it answered 2xx
print(res.status) -- 200, 404, 500… (0 = no answer)
print(res.body) -- text
print(res.headers["content-type"]) -- names are lower-case
print(res.error) -- nil when it answered at all
end)A 404 is not an error. Neither is a 500. Those are answers, and res.error stays nil for both — check res.ok or res.status for what the far end said. res.error is for when there was no answer: a timeout, a refused connection, a body too big to accept.
Refusals
send answers nil and a reason instead of raising, like everything else in this API. A refusal here means the callback will not run — nothing was sent.
| reason | what to do |
|---|---|
permission | the world has not granted net to this script's owner |
http busy | too many requests waiting; back off and retry |
http cooling | that address has been failing, and is left alone for 30 s |
http quota | the world has spent its traffic for the day |
too many requests in one event | an event may change at most 8 things, and a request is one |
http: … | the request itself: the address, a header or a size. The text says which |
Addresses that are refused
luau
http:send({ url = "http://api.example.com/" }) -- nil, only https
http:send({ url = "https://api.example.com:8080/" })-- nil, only port 443
http:send({ url = "https://127.0.0.1/" }) -- nil, not reachable
http:send({ url = "https://user:[email protected]/" }) -- nil, no userinfoOnly https://, only port 443, and only addresses out on the public internet. An address that points inside the machine hosting the world is refused, and so is a name that resolves to one — the check happens against the address the connection is actually about to use.
Plain http:// is not allowed on purpose. In plain text, anyone between the world and the far end can not only read the answer but rewrite it, and the answer is what your script then acts on.
Redirects are not followed
A 3xx comes back as an ordinary answer. Read it and decide:
luau
http:send({ url = first }, function(res)
if res.status == 301 or res.status == 302 then
local where = res.headers["location"]
if where then
http:send({ url = where }, function(res2) end)
end
end
end)Headers the engine owns
host, content-length, connection, transfer-encoding, upgrade, expect, cookie and anything starting proxy- or x-hungrit- are set by the engine and cannot be changed. There are no cookies at all — every request stands alone.
Every request carries x-hungrit-world and x-hungrit-object, so a service you call can tell your world apart from every other one.
An API key in a script is a public API key
Scripts are open. Anybody who can read yours can read the token in it. Until there is a place to keep a secret, treat any key you put in a script as published — and prefer a service of your own in front of the one holding the real key.
Limits
See limits for the full table. In short: 10 requests a second per world, banking up to 60 for a burst; 64 KB out and 256 KB back; 10 seconds before it gives up.
