Данные

HTTP

Плагин обращается к веб-сервисам через две функции, обе глобальные — без строки импорта:

  • useFetch — повседневная: один вызов отправляет запрос и читает JSON ответа в ваш интерфейс. Она никогда не бросает исключение: что пошло не так — в результате.
  • fetch — та самая функция браузера, когда нужен ответ как есть: его статус, заголовки и тело.

Запрос идёт в фоне. Игра его не ждёт: ответ приходит на одном из следующих кадров сервера, никогда — внутри вызова, который отправил запрос.

useFetch с 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
, `Погоды нет: ${
error
.
message
}`);
print
(
player
, `${
data
!.
temperature
} °C, ${
data
!.
description
}`);
});

useFetch<T>(url, options) даёт { data, error, status }:

  • data — JSON ответа, прочитанный в T: интерфейс с полями этого JSON, объявленный рядом с вашим кодом. null, если есть ошибка или у ответа нет тела.
  • error — null или причина, по которой данных нет:
    errorкогда
    FetchErrorсервер ответил статусом не из 2xx; error.status, error.statusText и error.body — часто там сервер пишет, что не так
    TypeErrorответа нет: нет соединения, плохой адрес, сертификат, за который не ручается ни один доверенный центр; или в JSON нет поля, которое требует T
    SyntaxErrorтело — не JSON
    TimeoutError, AbortErrorистёк timeout или сработал signal
  • status — HTTP-статус ответа, 0, если ответа не было.

Поле, которого в JSON может не быть, — необязательное поле интерфейса (description?: string); поле, которого интерфейс не называет, пропускается.

НастройкаЗначение
methodметод запроса: по умолчанию "GET", "POST", "PUT", "PATCH", "DELETE", ...
queryимена и значения, добавляемые к адресу: { page: "2" } добавляет ?page=2
headersзаголовки запроса объектом: { Authorization: "Bearer abc" }
bodyчто отправить: текст как есть или объект в виде JSON (ниже)
retryсколько раз попробовать ещё, если запрос сорвался по пути или сервер ответил 408, 409, 425, 429, 500, 502, 503 или 504; по умолчанию 0
retryDelayмиллисекунды между попытками; по умолчанию 500
timeoutсколько миллисекунд может длиться попытка; потом она заканчивается TimeoutError
signalAbortSignal, который отменяет запрос
proxy, tlsкак у fetch (ниже)

useFetch просит JSON (Accept: application/json). Чтобы прочитать тело текстом, передайте string как T: useFetch<string>(url).

Отправка данных

Объект уходит как JSON, с Content-Type: application/json. Его тип — второй аргумент типа:

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,
});

Текст уходит как есть: body: "hello", с одним useFetch<Saved>.

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) принимает адрес текстом, URL или Request (new Request(url, init)) и даёт промис Response.

initЗначение
methodметод запроса; по умолчанию "GET"
headersзаголовки запроса, объектом имён и значений
bodyтекст, который уходит с запросом; у запроса GET или HEAD его нет. Без заголовка Content-Type он уходит как text/plain;charset=UTF-8
redirect"follow" (по умолчанию) идёт по перенаправлению, до 20 раз; "manual" отдаёт само перенаправление ответом; "error" отклоняет
signalAbortSignal, который отменяет запрос
proxyпрокси, через который идёт запрос (ниже)
tls{ ca }: центры сертификации, с которыми сверяется сертификат сервера (ниже)

Ответ:

  • response.status и response.statusText — статус и его слова (404, "Not Found"); response.ok равно true от 200 до 299.
  • response.headers — его заголовки: get(name) в любом регистре, has, forEach, entries и getSetCookie() для всех Set-Cookie.
  • response.url — адрес, с которого он пришёл после перенаправлений, а response.redirected говорит, были ли они.
  • Тело читается один раз: await response.text(), await response.json<T>() или await response.arrayBuffer(); clone() делает копию, чтобы прочитать дважды.

Статус вроде 404 или 500 — это ответ, как в браузере: проверяйте response.ok. Промис отклоняется, когда ответа нет вовсе, — с TypeError, в сообщении которого сказано почему (fetch failed: Could not resolve host: ...), — и когда срабатывает сигнал.

Адреса и запросы

URL разбирает адрес на части, а searchParams собирает его запрос:

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) достраивает адрес от другого (new URL("../users", "https://example.com/api/v1/")); URL.canParse и URL.parse проверяют адрес без try. new URLSearchParams({ ... }) делает отдельный запрос, который ${query} записывает текстом:

const 
query
= new
URLSearchParams
({
steamid
:
player
.
steamId
,
map
:
server
.
map
});
await
fetch
(`https://example.com/api/rank?${
query
}`);

Отмена и таймауты

signal отменяет запрос, и промис отклоняется с ошибкой AbortError — или TimeoutError для AbortSignal.timeout(ms):

const 
response
= await
fetch
(url, {
signal
: AbortSignal.timeout(5000) }); // сдаться через 5 с

У useFetch то же самое — настройка timeout, на каждую попытку. В async-обработчике команды или события про игрока запрос отменяется сам, когда игрок уходит (async).

HTTPS и прокси

Адрес https:// сверяется с центрами сертификации, которым доверяет браузер; их список несёт серверный модуль, так что серверу для этого ничего ставить не нужно. Сервису с собственным сертификатом — панели на вашей же машине — доверяют, передав этот сертификат текстом PEM в 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 отправляет запрос через прокси — http://, https:// или socks5://, с пользователем и паролем в адресе, если они нужны:

await 
useFetch
<Status>(url, {
proxy
: "http://user:secret@proxy.example.com:3128" });

Без proxy запрос идёт напрямую; переменная окружения сервера http_proxy не используется.

fetch сервера — не браузерный, и кое-чего из того, что делает браузер, в нём нет:
  • Нет CORS. Сервер — не веб-страница: доступен любой адрес и читается любой ответ. mode и credentials нет.
  • Нет хранилища cookie. Set-Cookie ответа не сохраняется и не отправляется снова. Прочитайте его через response.headers.getSetCookie() и отправьте обратно заголовком Cookie сами.
  • Нет кэша. Каждый запрос идёт в сеть.
  • Ответ читается целиком. Тело приходит сразу, не больше 64 МБ, и плагин держит его в своей памяти: большие файлы — не для плагина.