HTTP
A plugin talks to web services with two functions, both globals - no import line:
useFetch- the everyday one: one call sends the request and reads the JSON of the response into your interface. It never throws: what went wrong is in the result.fetch- the browser's own function, for the response as it is: its status, headers and body.
A request runs in the background. The game does not wait for it: the response arrives on a later server frame, never inside the call that sent the request.
useFetch since v0.2
interface Weather {
temperature: number;
description: string;
}
server.addCommand("/weather", async ({ player }) => {
const { data, error } = await useFetch<Weather>("https://example.com/api/weather", {
query: { city: "Paris" },
});
if (error) return print(player, `No weather: ${error.message}`);
print(player, `${data!.temperature} °C, ${data!.description}`);
});
useFetch<T>(url, options) gives { data, error, status }:
datais the response's JSON read intoT- an interface of the fields the JSON has, declared beside your code.nullwhen there is an error or the response has no body.errorisnull, or why there is no data:error when FetchErrorthe server answered with a status outside 2xx;error.status,error.statusTextanderror.body- often what the server says went wrongTypeErrorthere is no response: no connection, a bad address, a certificate no trusted authority vouches for; or the JSON lacks a field TrequiresSyntaxErrorthe body is not JSON TimeoutError,AbortErrortimeoutran out, orsignalabortedstatusis the response's HTTP status,0when there was no response.
A field the JSON may leave out is an optional one in the interface
(description?: string); a field the interface does not name is skipped.
| Option | Meaning |
|---|---|
method | the request's method: "GET" by default, "POST", "PUT", "PATCH", "DELETE", ... |
query | names and values added to the address: { page: "2" } adds ?page=2 |
headers | the request's headers, as an object: { Authorization: "Bearer abc" } |
body | what is sent: text as it is, or an object as JSON (below) |
retry | how many more times to try when the request fails on the way or the server answers 408, 409, 425, 429, 500, 502, 503 or 504; 0 by default |
retryDelay | milliseconds between two tries; 500 by default |
timeout | milliseconds a try may take; it then ends with a TimeoutError |
signal | an AbortSignal that cancels the request |
proxy, tls | as in fetch (below) |
useFetch asks for JSON (Accept: application/json). To read the body as
text, give string as T: useFetch<string>(url).
Sending data
An object goes as JSON, with Content-Type: application/json. Its type is
the second type argument:
interface Report {
map: string;
players: number;
}
interface Saved {
id: number;
}
const report: Report = { map: server.map, players: server.players.length };
const { data, error } = await useFetch<Saved, Report>("https://example.com/api/reports", {
method: "POST",
body: report,
retry: 2,
});
Text is sent as it is: body: "hello", with useFetch<Saved> alone.
fetch
interface Answer {
accepted: boolean;
}
const response = await fetch("https://example.com/api/stats", {
method: "POST",
headers: { Authorization: "Bearer abc", "Content-Type": "application/json" },
body: JSON.stringify(stats),
});
if (!response.ok) return console.log(`HTTP ${response.status} ${response.statusText}`);
const answer = await response.json<Answer>();
fetch(input, init) takes an address as text, a URL
or a Request (new Request(url, init)), and gives a promise of the
Response.
init | Meaning |
|---|---|
method | the request's method; "GET" by default |
headers | the request's headers, as an object of names and values |
body | the text sent with the request; a GET or HEAD request has none. Without a Content-Type header it goes as text/plain;charset=UTF-8 |
redirect | "follow" (the default) follows a redirect, up to 20 of them; "manual" gives the redirect itself as the response; "error" rejects |
signal | an AbortSignal that cancels the request |
proxy | a proxy the request goes through (below) |
tls | { ca }: the certificate authorities a server's certificate is checked against (below) |
The response:
response.statusandresponse.statusTextare the status and its words (404,"Not Found");response.okistruefor200to299.response.headersare its headers:get(name)in any case,has,forEach,entries, andgetSetCookie()for everySet-Cookie.response.urlis the address it came from after any redirects, andresponse.redirectedsays whether there were any.- The body is read once, with
await response.text(),await response.json<T>()orawait response.arrayBuffer();clone()makes a copy to read it twice.
A status such as 404 or 500 is a response, as in the browser: check
response.ok. The promise is rejected when there is no response at all -
with a TypeError whose message says why (fetch failed: Could not resolve host: ...) - and when a signal aborts.
Addresses and queries
URL takes an address apart, and searchParams builds its query:
const url = new URL("https://example.com/api/rank");
url.searchParams.set("steamid", player.steamId);
url.searchParams.set("map", server.map);
const response = await fetch(url);
new URL(path, base) resolves an address against another
(new URL("../users", "https://example.com/api/v1/")); URL.canParse and
URL.parse check one without a try. new URLSearchParams({ ... }) makes a
query of its own, which ${query} writes as text:
const query = new URLSearchParams({ steamid: player.steamId, map: server.map });
await fetch(`https://example.com/api/rank?${query}`);
Cancelling and timeouts
A signal cancels a request, and the promise is rejected with an error
named AbortError - TimeoutError for AbortSignal.timeout(ms):
const response = await fetch(url, { signal: AbortSignal.timeout(5000) }); // give up after 5 s
useFetch has the same as its timeout option, for each try. In an async
command handler or a listener of an event about a player, the player leaving
cancels the request by itself
(async).
HTTPS and proxies
An https:// address is checked against the certificate authorities a
browser trusts; the server module carries their list, so a server needs
nothing installed for it. A service with a certificate of its own making -
a panel on your own machine - is trusted by giving that certificate, as PEM
text, in tls.ca:
import * as fs from "@amxts/core/fs";
const ca = fs.readFileSync(`${server.configsDir}/panel.pem`) ?? "";
const response = await fetch("https://panel.example.com/api/bans", { tls: { ca } });
proxy sends a request through a proxy - http://, https:// or
socks5://, with a user and password in the address when it needs them:
await useFetch<Status>(url, { proxy: "http://user:secret@proxy.example.com:3128" });
Without proxy a request goes straight out; a server's http_proxy
environment variable is not used.
fetch is not a browser's, and some of what a browser does is not
there:- No CORS. A server is no web page: every address is reachable and every
response readable.
modeandcredentialsdo not exist. - No cookie jar. A
Set-Cookieof a response is not kept and not sent again. Read it withresponse.headers.getSetCookie()and send it back as aCookieheader yourself. - No cache. Every request goes to the network.
- A response is read whole. Its body arrives at once, at most 64 MB, and a plugin keeps it in its own memory: large files are not for a plugin.