Data

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 }:

  • data is the response's JSON read into T - an interface of the fields the JSON has, declared beside your code. null when there is an error or the response has no body.
  • error is null, or why there is no data:
    errorwhen
    FetchErrorthe server answered with a status outside 2xx; error.status, error.statusText and error.body - often what the server says went wrong
    TypeErrorthere is no response: no connection, a bad address, a certificate no trusted authority vouches for; or the JSON lacks a field T requires
    SyntaxErrorthe body is not JSON
    TimeoutError, AbortErrortimeout ran out, or signal aborted
  • status is the response's HTTP status, 0 when 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.

OptionMeaning
methodthe request's method: "GET" by default, "POST", "PUT", "PATCH", "DELETE", ...
querynames and values added to the address: { page: "2" } adds ?page=2
headersthe request's headers, as an object: { Authorization: "Bearer abc" }
bodywhat is sent: text as it is, or an object as JSON (below)
retryhow 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
retryDelaymilliseconds between two tries; 500 by default
timeoutmilliseconds a try may take; it then ends with a TimeoutError
signalan AbortSignal that cancels the request
proxy, tlsas 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.

initMeaning
methodthe request's method; "GET" by default
headersthe request's headers, as an object of names and values
bodythe 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
signalan AbortSignal that cancels the request
proxya proxy the request goes through (below)
tls{ ca }: the certificate authorities a server's certificate is checked against (below)

The response:

  • response.status and response.statusText are the status and its words (404, "Not Found"); response.ok is true for 200 to 299.
  • response.headers are its headers: get(name) in any case, has, forEach, entries, and getSetCookie() for every Set-Cookie.
  • response.url is the address it came from after any redirects, and response.redirected says whether there were any.
  • The body is read once, with await response.text(), await response.json<T>() or await 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.

A server's 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. mode and credentials do not exist.
  • No cookie jar. A Set-Cookie of a response is not kept and not sent again. Read it with response.headers.getSetCookie() and send it back as a Cookie header 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.