Reaching websites#
http is the global a server script uses to call an address on the web and read what comes back: a
webhook, a score board of your own, a Discord message. Client scripts cannot; a player's machine
never makes the call, so nothing a place asks for depends on what is installed on their computer or
what their network allows.
Turning it on#
Requests are off until the place asks for them. Add the feature to place.toml:
[features]
httpRequests = trueIn the editor the same switch is Project Settings > Scripts > httpRequests. Without it every request errors with "http requests are off".
Asking#
-- server
task.spawn(function()
local reply = http.get("https://example.com/scores")
print(reply.status, reply.ok)
local scores = http.jsonDecode(reply.body)
end)The call runs inside task.spawn because it waits for the website to answer. While it waits, the
rest of the place keeps running: the tick carries on, other players are served, and your script
resumes on the line after the call once the answer arrives.
There are three ways to ask, and they differ only in how much you spell out:
| Call | |
|---|---|
http.get(url, headers?) |
|
http.post(url, body, contentType?, headers?) |
contentType is application/json when left out |
http.request({ url = ..., method = ..., headers = ..., body = ... }) |
method is GET when left out |
Use http.request when you need a method the other two do not cover, or when you want to build the
request as a table. method is one of GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS.
A method the engine does not know is an error that says so.
Each of them hands back one table:
| Field | |
|---|---|
status |
the code, like 200 or 404 |
ok |
true for 200 to 299 |
body |
the answer as text |
headers |
the answer's headers, joined with , where one was sent twice |
body is always text. If the site answered with JSON, run it through http.jsonDecode yourself.
A request waits, so it runs inside a function started with task.spawn, like anything else that
waits. Calling it straight out of game.stepped holds up the tick everybody else is on.
Helpers#
These turn values into text and back, and generate the ids and escaped strings a web call usually needs. They work on both sides, with the feature off, and never wait:
http.jsonEncode({ name = "meek", score = 12 }) -- '{"name":"meek","score":12}'
http.jsonDecode('{"ok":true}').ok -- true
http.generateGuid() -- "{2E3A...}"
http.generateGuid(false) -- the same without the braces
http.urlEncode("a b&c") -- "a%20b%26c"jsonEncodetakes what a store takes: numbers, text, booleans and nested tables with text keys.jsonDecodeerrors on text it cannot read, so catch it withpcallwhen the text came from outside.jsonDecodeand its XML twinxml.decodealso read files that ship with the place, throughfiles.read. See Reading files.generateGuidgives you a unique id, with braces by default and without them when you passfalse.urlEncodeescapes a value so you can put it in a query string without the spaces and ampersands breaking the address.
Catch what you send#
A website can be down, slow or angry, and a request that fails raises in the caller. Wrap it:
-- server
local hook = "https://example.com/hooks/rounds"
local function announce(winner: string, seconds: number)
task.spawn(function()
local ok, reply = pcall(function()
return http.post(hook, http.jsonEncode({
content = winner .. " won in " .. math.floor(seconds) .. " seconds",
}))
end)
if not ok then
print("the webhook did not go through:", reply)
elseif not reply.ok then
print("the webhook answered", reply.status, reply.body)
end
end)
endThere are two ways this goes wrong, and you check them separately:
- The call never got an answer at all, because the address is unreachable or it ran out of time.
pcallreturnsfalseand the second value is the error. - The call got an answer and the answer was a refusal, such as 401 or 429.
pcallreturnstrue, andreply.okis false.reply.statusandreply.bodysay why.
Websockets#
A request asks once and gets one answer. A websocket stays open, and either end can send whenever it
likes, which is what a live feed needs: a chat bridge, a match server of your own, a dashboard that
pushes commands to your game. http.websocket opens one from a server script, under the same
httpRequests switch as everything else here:
-- server
task.spawn(function()
local feed = http.websocket("wss://example.com/feed", {
headers = { Authorization = "Bearer " .. token },
reconnect = true,
})
feed.message:connect(function(text)
local event = http.jsonDecode(text)
if event.kind == "announce" then
game.chat:system(event.text)
end
end)
feed.closed:connect(function(code, reason, reconnecting)
print("the feed closed", code, reason, reconnecting)
end)
feed.reconnected:connect(function()
feed:send(http.jsonEncode({ kind = "hello", server = game.serverId }))
end)
feed:send(http.jsonEncode({ kind = "hello", server = game.serverId }))
end)http.websocket waits until the socket is open, so it runs inside task.spawn like a request, and
it errors if the address does not answer. What it hands back is the socket:
messagefires with every message that arrives, as text, or as abufferwhen the other end sent binary. Handlers run on the server's tick, never in the middle of one.sendtakes text or a buffer and sends it as it is; encode a table withhttp.jsonEncodefirst. Sending on a socket that is not open is an error.closedfires with the close code, the reason and whether Moud will try again.close(code?, reason?)closes it for good. Stopping the script that opened it closes it too.openis true while it is connected, andurlis where it goes.
With reconnect = true a socket that drops without your script closing it is opened again after one
second, then two, four, and up to thirty seconds between tries. reconnected fires when it is back,
which is where you say hello again, since the other end sees a fresh connection. Without it, call
feed:reconnect() yourself; it waits until the socket is open again.
message handlers cannot wait, like any other signal handler. Start a task.spawn inside one when
what you do with a message has to wait, such as a request.
Only ws:// and wss:// addresses open a websocket. Client scripts cannot open one, for the same
reason they cannot make requests.
Limits#
- Only
http://andhttps://addresses. Anything else is an error before the call leaves. - 30 seconds to connect, 30 more for the answer to start, and a minute for the whole answer. A request that runs out of time errors.
- 16 MB each way. A longer body is refused before it leaves, and a longer answer is an error. A websocket message has the same limit.
- Redirects are followed, except from
https://tohttp://. - Every address is reachable, your own machine and local network included.
httpRequestsis the only switch. - A player is never told what the server asked or what it heard. Send it over a channel yourself if they should see it.
Treat everything in body as something a stranger wrote. Check it before you put it in the world:
a name you paste into a TextLabel, a number you feed to cframe, or a block id you look up all
come from somebody outside your place.
