Fetch API: HTTP-ошибки, чтение ответа, отмена и гонки запросов
Автор: Казачкин Даниил Михайлович · Обновлено
Fetch API выполняет запрос ресурса и возвращает Promise с объектом Response. Надёжная загрузка отдельно проверяет транспортный результат, HTTP-статус, формат тела и актуальность…
Fetch API выполняет запрос ресурса и возвращает Promise с объектом Response. Надёжная загрузка отдельно проверяет транспортный результат, HTTP-статус, формат тела и актуальность данных для текущего состояния интерфейса.
Среда и самостоятельная проверка
Первый блок сохраните как fetch-demo.mjs и выполните node fetch-demo.mjs в Node.js 22 или новее. Он использует встроенные Fetch API, Response и AbortController. Основной ресурс задан через data:, поэтому внешний сервер и интернет не нужны. Ответ с кодом 404 создаётся явно как модель HTTP-ответа: этот участок проверяет обработку статуса, а не сетевой обмен.
В браузере fetch может получать ресурс по относительному URL текущего документа, а в самостоятельном Node-примере используем полный адрес. По умолчанию обычный HTTP-вызов выполняет GET. Дополнительные параметры задают метод, заголовки, тело и сигнал отмены. Объект Response содержит метаданные ответа; его получение ещё не означает завершённое чтение и разбор тела.
async function decodeJson(response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
}
const url = 'data:application/json,' + encodeURIComponent('{"title":"Fetch"}');
console.log((await decodeJson(await fetch(url))).title);
const missing = await Promise.resolve(new Response('missing', { status: 404 }));
console.log(missing.status, missing.ok);
try {
await decodeJson(missing);
} catch (error) {
console.log(error.message);
}
const controller = new AbortController();
controller.abort();
try {
await fetch(url, { signal: controller.signal });
} catch (error) {
console.log(error.name);
}Ожидаемый вывод: Fetch, 404 false, HTTP 404, AbortError. Promise с созданным ответом 404 успешно выполнен, как и обычный fetch, получивший такой HTTP-ответ. Ошибку приложения создаёт наша проверка. Сигнал в последнем опыте отменён заранее, поэтому отказ не зависит от скорости передачи данных. Для новой попытки нужен новый контроллер: отменённый сигнал нельзя вернуть в исходное состояние.
Четыре разных границы отказа
Сетевая ошибка, неподдерживаемая схема или блокировка доступа могут отклонить Promise самого fetch. HTTP-код 404 или 500 означает, что ответ получен, поэтому проверяется через status или ok. Успешный статус с некорректным JSON приводит к ошибке чтения или разбора тела. Наконец, синтаксически корректный JSON может не соответствовать ожидаемой структуре: например, поле items оказалось строкой.
response.json() не проверяет предметную схему и не делает данные типобезопасными. Проверяйте обязательные поля на границе приложения или используйте предусмотренный проектом валидатор. Для ответа без тела, например ожидаемого 204, не вызывайте JSON-разбор безусловно. Контракт конкретного endpoint должен определять, возвращается ли полезное значение вообще.
Тело ответа обычно представляет потребляемый поток. Нельзя сначала прочитать его как текст для логирования, а затем без подготовки снова вызвать json. Если действительно нужны два потребителя, изучите clone и стоимость буферизации; чаще проще прочитать один раз и передать результат. Не записывайте полные ответы с персональными данными ради диагностики формата — достаточно статуса, типа содержимого и безопасного описания ошибки.
Отмена и актуальность — разные условия
AbortController сообщает поддерживающей операции, что она больше не нужна. Если отмена происходит во время чтения тела, это чтение также может завершиться отказом. Если результат уже вычислен и код продолжил собственную обработку, отмена не перематывает исполнение назад. Она также не является гарантией отката действия на сервере: запрос записи мог быть принят до разрыва ожидания.
Поисковый интерфейс требует дополнительного правила: старый результат не должен заменять новый. Обычно используют возрастающий номер запроса и проверяют его непосредственно перед записью состояния. Проверка нужна и в ветке ошибки, и в finally, если там меняется общий индикатор загрузки. Иначе старый запрос может не показать данные, но всё равно погасить индикатор нового.
Следующий самостоятельный пример намеренно моделирует источник, который игнорирует отмену. Это позволяет проверить защиту актуальности независимо от поведения сети. Обе операции завершаются вручную в обратном порядке, без случайных таймеров.
function createLatestLoader(load, publish) {
let revision = 0;
let current;
return {
async run(key) {
const ownRevision = ++revision;
current?.abort();
const controller = new AbortController();
current = controller;
try {
const value = await load(key, controller.signal);
if (ownRevision === revision && !controller.signal.aborted) publish(value);
} catch (error) {
if (ownRevision === revision && !controller.signal.aborted) throw error;
} finally {
if (ownRevision === revision) current = undefined;
}
},
dispose() { revision += 1; current?.abort(); current = undefined; },
};
}
const pending = new Map();
const published = [];
const loader = createLatestLoader(
(key) => new Promise((resolve) => pending.set(key, resolve)),
(value) => published.push(value),
);
const oldRun = loader.run('old');
const newRun = loader.run('new');
pending.get('new')('новый результат');
await newRun;
pending.get('old')('старый результат');
await oldRun;
loader.dispose();
console.log(published.join(','));Единственный вывод — новый результат. В настоящей загрузке передайте сигнал в fetch, проверьте HTTP-статус и схему, а публикацию оставьте под защитой номера операции. Ошибку актуального run должен обработать вызывающий код. После dispose уже начатое продолжение не сможет опубликовать данные; создание следующего экземпляра относится к новому жизненному циклу экрана.
Тайм-аут и повторные попытки
Тайм-аут можно связать с отменой, а при ручном таймере обязательно очищать его в finally. Простое соревнование с таймером через Promise.race прекращает ожидание, но само по себе не отменяет запрос. Если добавляете собственный обработчик abort, удаляйте его и при обычном успехе: параметр once срабатывает только когда событие действительно произошло.
Автоматический повтор требует понимания операции. Повторный GET и повторное создание заказа имеют разные последствия. Не повторяйте любую ошибку одинаково; определите допустимые статусы, предел попыток и способ избежать дублирующего изменения. Для отмены из-за ухода пользователя повтор обычно противоречит его действию. Эти решения должны находиться в одном понятном слое, а не независимо в каждом компоненте.
Практика: настоящий локальный 404
В пустой учебной папке создайте lesson.json с содержимым {"title":"Локальный урок"}. Запустите там python3 -m http.server 8080 --bind 127.0.0.1, откройте http://127.0.0.1:8080/ и в консоли вызовите fetch('./lesson.json'), затем fetch('./missing.json'). У первого ожидается статус 200, у второго 404; оба дают Response. Передайте ответы в функцию decodeJson из первого блока и сравните обработку. После опыта остановите учебный сервер через Ctrl+C.
Частые вопросы
Почему no-cors не даёт прочитать ответ?
Этот режим не отключает политику браузера и обычно приводит к непрозрачному ответу для соответствующего межсайтового запроса. Исправлять нужно контракт доступа сервера или архитектуру обращения. Подробности CORS и preflight разобраны в отдельном уроке проекта.
Всегда ли отмена имеет имя AbortError?
При обычном abort() используется такая причина, но abort(reason) допускает собственное значение. В коде, владеющем контроллером, учитывайте состояние сигнала и свой контракт причин; не предполагайте, что всякая ошибка обязательно является объектом Error.
Почему после быстрого ввода показался старый ответ?
Порядок завершения запросов не обязан совпадать с порядком запуска. Отмена экономит ненужную работу, а проверка актуальности защищает запись состояния. Проверьте обе части, включая обработку ошибок и индикатора ожидания.
Связанные исследования
- Рост памяти и утечка в JavaScript: как проверить гипотезу удержания — Метод диагностики удержания памяти: одинаковые циклы жизненного цикла, контрольный режим очистки, пути ссылок, retained size и ограничения RSS и heap snapshots.