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 нет поля, которое требует TSyntaxErrorтело — не JSON TimeoutError,AbortErrorистёк timeoutили сработалsignalstatus— 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 |
signal | AbortSignal, который отменяет запрос |
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" отклоняет |
signal | AbortSignal, который отменяет запрос |
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 МБ, и плагин держит его в своей памяти: большие файлы — не для плагина.