Fetch API: современные сетевые запросы в JavaScript

Раздел: Веб-разработка -> HTTP

Основы работы с Fetch API

Fetch API предоставляет современный интерфейс для выполнения сетевых запросов в браузере. Он основан на объектах Request и Response, а также использует промисы для асинхронной работы. В отличие от устаревшего XMLHttpRequest, Fetch имеет более простой синтаксис и встроенную поддержку промисов. Цель статьи – разобрать типичные сценарии использования, включая обработку ошибок, отправку данных и работу с разными форматами ответов.

Как выполнить простой GET-запрос и получить данные?

Самый распространённый случай – загрузка данных с сервера. Базовый код выглядит так:


fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Ошибка:', error));

Fetch js (fetch api в javascript)

Пояснение по шагам:

  • Функция fetch принимает URL и возвращает промис, который разрешается объектом Response.
  • Метод response.json() также возвращает промис, который преобразует тело ответа в JavaScript-объект.
  • Второй then использует полученные данные, например, выводит их в консоль.
  • Блок catch перехватывает ошибки сети или некорректный парсинг JSON.

Типичная ошибка

Новички часто забывают, что fetch не выдаёт ошибку при статусах 4xx или 5xx – промис отклоняется только при отсутствии ответа от сервера (например, обрыв сети). Для проверки статуса нужно добавить условие:


fetch(url)
  .then(response => {
    if (!response.ok) throw new Error('HTTP error ' + response.status);
    return response.json();
  })
  .then(data => console.log(data))
  .catch(err => console.error(err));

Js get (get запрос в javascript)

Как отправить POST-запрос с телом JSON?

Для отправки данных часто используется метод POST. Необходимо передать объект с настройками:


fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Иван', age: 30 })
})
  .then(response => response.json())
  .then(result => console.log(result))
  .catch(error => console.error('Ошибка:', error));

Requests js (http-запросы на javascript)

В объекте параметров обязательно указывается method и body. Заголовок Content-Type сообщает серверу, что данные передаются в формате JSON. Тело должно быть сериализовано с помощью JSON.stringify.

Проблема

Если забыть указать заголовок Content-Type, сервер может не распознать тело запроса как JSON и вернуть ошибку 415 (Unsupported Media Type) или неправильно обработать данные.

Как использовать async/await для упрощения кода?

Синтаксис async/await делает код с промисами более читаемым и похожим на синхронный:


async function loadData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) throw new Error('Ошибка ' + response.status);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Не удалось загрузить данные:', error);
  }
}

Функция loadData объявлена с ключевым словом async. Внутри неё await приостанавливает выполнение до разрешения промиса. Обработка ошибок выполняется с помощью try/catch.

Этот подход особенно удобен при цепочке нескольких запросов, где результат одного используется в другом.

Как передать пользовательские заголовки в запросе?

Для работы с авторизацией или другими кастомными заголовками используется объект headers:


fetch('https://api.example.com/private', {
  headers: {
    'Authorization': 'Bearer my-token',
    'X-Custom-Header': 'custom-value'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Заголовки можно также задать с помощью конструктора Headers:


const myHeaders = new Headers();
myHeaders.append('Authorization', 'Bearer my-token');

fetch(url, { headers: myHeaders })
  .then(response => response.json())
  .then(data => console.log(data));

Это полезно, когда требуется много заголовков или их нужно динамически изменять.

Ошибка CORS

Если сервер не разрешает указанные заголовки через заголовок Access-Control-Allow-Headers, браузер заблокирует запрос. В таком случае нужно либо изменить настройки сервера, либо использовать прокси.

Как отменить запрос с помощью AbortController?

Иногда необходимо отменить выполняющийся запрос, например, когда пользователь покидает страницу или меняет фильтры. Для этого используется AbortController:


const controller = new AbortController();
const signal = controller.signal;

fetch(url, { signal })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('Запрос отменён');
    } else {
      console.error('Ошибка:', err);
    }
  });

// Отмена запроса
controller.abort();

При вызове abort() промис отклоняется с ошибкой AbortError. Это позволяет корректно обрабатывать отмену и избегать лишних операций.

Как обработать разные типы ответов (text, blob, formData)?

Метод response.json() – только один из способов извлечения данных. В зависимости от Content-Type можно использовать:

  • response.text() – для получения тела как строки (например, HTML или CSV).
  • response.blob() – для бинарных данных (изображения, PDF).
  • response.formData() – для ответов, содержащих multipart/form-data.
  • response.arrayBuffer() – для низкоуровневого доступа к бинарным данным.

Пример загрузки изображения как blob:


fetch('https://example.com/logo.png')
  .then(response => response.blob())
  .then(blob => {
    const imgUrl = URL.createObjectURL(blob);
    document.getElementById('myImage').src = imgUrl;
  })
  .catch(error => console.error(error));

Как отправлять FormData (файлы)?

Для отправки форм, содержащих файлы, используется объект FormData:


const formData = new FormData();
formData.append('username', 'ivan');
formData.append('avatar', fileInput.files[0]);

fetch('https://api.example.com/upload', {
  method: 'POST',
  body: formData // заголовок Content-Type устанавливается автоматически как multipart/form-data
})
  .then(response => response.json())
  .then(result => console.log(result))
  .catch(error => console.error(error));

При использовании FormData не нужно явно указывать заголовок Content-Type – браузер сам добавит правильный тип с границей. Если указать свой, это может сломать запрос.

Как включить credentials (куки, авторизацию) в кросс-доменных запросах?

По умолчанию fetch не отправляет куки или другие учётные данные на другие домены. Чтобы включить их, добавляется параметр credentials:


fetch('https://api.example.com/account', {
  credentials: 'include'
})
  .then(response => response.json())
  .then(data => console.log(data));

Значение 'include' заставляет браузер отправлять куки даже для запросов на другие источники. Сервер также должен ответить с заголовком Access-Control-Allow-Credentials: true. Альтернативные варианты: 'same-origin' (для запросов на тот же источник) и 'omit' (никогда не отправлять).

Проблема

Если сервер не настроен на получение credentials в кросс-доменном запросе, браузер отклонит запрос и не передаст данные. При этом Access-Control-Allow-Origin не может быть '*', должен быть конкретный источник.

Как реализовать тайм-аут для запроса?

Встроенной поддержки тайм-аута у fetch нет, но её легко реализовать через AbortController и setTimeout:


function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController();
  setTimeout(() => controller.abort(), timeout);

  return fetch(url, { signal: controller.signal })
    .then(response => response.json());
}

fetchWithTimeout('https://api.example.com/slow', 3000)
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.error('Запрос превысил время ожидания');
    } else {
      console.error('Ошибка:', err);
    }
  });

Функция создаёт контроллер, устанавливает таймер на отмену и передаёт сигнал в fetch. Если запрос не успеет завершиться до тайм-аута, он будет отменён.

Расширенные примеры и нестандартные сценарии

Как выполнить цепочку последовательных запросов?

Иногда требуется сначала получить один ресурс, а затем, используя его данные, запросить другой. С помощью async/await это становится очевидным:

Пример

async function getUserPosts(userId) {
  try {
    const userResponse = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
    if (!userResponse.ok) throw new Error('Пользователь не найден');
    const user = await userResponse.json();

    const postsResponse = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`);
    if (!postsResponse.ok) throw new Error('Не удалось загрузить посты');
    const posts = await postsResponse.json();

    console.log('Имя пользователя:', user.name);
    console.log('Количество постов:', posts.length);
    return { user, posts };
  } catch (error) {
    console.error('Ошибка в цепочке:', error);
  }
}

getUserPosts(1);

Результат (пример вывода):

Имя пользователя: Leanne Graham
Количество постов: 10

Как выполнить несколько параллельных запросов и дождаться всех?

Для параллельной загрузки используется Promise.all:

Пример

async function fetchMultiple() {
  const urls = [
    'https://api.github.com/users/octocat',
    'https://api.github.com/users/defunkt'
  ];

  try {
    const promises = urls.map(url => fetch(url).then(res => {
      if (!res.ok) throw new Error('Ошибка загрузки ' + url);
      return res.json();
    }));

    const [user1, user2] = await Promise.all(promises);
    console.log('Первый пользователь:', user1.login);
    console.log('Второй пользователь:', user2.login);
  } catch (error) {
    console.error('Один из запросов завершился ошибкой:', error);
  }
}

fetchMultiple();

Результат:

Первый пользователь: octocat
Второй пользователь: defunkt

Если один запрос провалится, Promise.all отклонит весь промис. Альтернатива – Promise.allSettled, позволяющий получить результаты всех запросов независимо от успеха.

Как отслеживать прогресс загрузки (download progress)?

Fetch API не предоставляет встроенного метода для отслеживания прогресса, но можно использовать ReadableStream и чтение порциями:

Пример

async function fetchWithProgress(url) {
  const response = await fetch(url);
  const contentLength = response.headers.get('Content-Length');
  const total = contentLength ? parseInt(contentLength, 10) : 0;
  let loaded = 0;

  const reader = response.body.getReader();
  const chunks = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    chunks.push(value);
    loaded += value.length;
    if (total) {
      const percent = Math.round((loaded / total) * 100);
      console.log(`Загружено ${percent}%`);
    }
  }

  // Собираем все чанки в один Uint8Array
  const allChunks = new Uint8Array(loaded);
  let position = 0;
  for (let chunk of chunks) {
    allChunks.set(chunk, position);
    position += chunk.length;
  }

  // Преобразуем в строку, если это текстовые данные
  const decoder = new TextDecoder('utf-8');
  const result = decoder.decode(allChunks);
  return result;
}

fetchWithProgress('https://example.com/large-file.txt')
  .then(data => console.log('Полные данные:', data.substring(0, 100)))
  .catch(error => console.error(error));

Этот пример демонстрирует работу с потоком, но на практике для большинства случаев достаточно использования response.blob() или response.text(). Метод с чтением потока полезен для очень больших файлов, когда нужно показывать прогресс или обрабатывать данные по мере поступления.

Как использовать разные HTTP методы (PUT, DELETE, PATCH)?

Fetch поддерживает любые методы, достаточно указать их в настройках:

Пример

// PUT - обновление ресурса
fetch('https://api.example.com/users/1', {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Иван Обновлённый' })
})
  .then(res => res.json())
  .then(data => console.log('Обновлено:', data))
  .catch(err => console.error(err));

// DELETE - удаление
fetch('https://api.example.com/users/1', {
  method: 'DELETE'
})
  .then(res => {
    if (res.ok) console.log('Ресурс удалён');
  })
  .catch(err => console.error(err));

// PATCH - частичное обновление
fetch('https://api.example.com/users/1', {
  method: 'PATCH',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ age: 31 })
})
  .then(res => res.json())
  .then(data => console.log('Обновлён возраст:', data));

Обратите внимание, что для PUT и PATCH необходимо передавать тело запроса, а для DELETE тело обычно не требуется.

Как работать с кэшированием ответов?

Fetch позволяет управлять кэшированием с помощью свойства cache:

Пример

// Попробовать взять из кэша, если нет – запросить с сервера
fetch(url, { cache: 'default' })
  .then(response => response.json())
  .then(data => console.log(data));

// Всегда запрашивать с сервера, игнорируя кэш
fetch(url, { cache: 'no-store' })
  .then(response => response.json())
  .then(data => console.log(data));

// Использовать кэш только если он свежий, иначе ошибка
fetch(url, { cache: 'only-if-cached' })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => console.log('Нет кэша или он устарел'));

Значения 'no-cache', 'force-cache' и 'only-if-cached' дают тонкий контроль. Однако поведение кэша может отличаться в зависимости от браузера и настроек сервера.

Как отправлять данные в формате URL-encoded (application/x-www-form-urlencoded)?

Иногда требуется отправить данные как стандартную форму (как если бы отправлялась HTML-форма). Для этого тело должно быть сериализовано соответствующим образом:

Пример

const params = new URLSearchParams();
params.append('name', 'Иван');
params.append('age', '30');

fetch('https://api.example.com/users', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: params
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));

Использование URLSearchParams автоматически кодирует ключи и значения, что удобно и безопасно.

Как получить заголовки ответа?

Доступ к заголовкам ответа осуществляется через объект response.headers:

Пример

fetch('https://api.github.com')
  .then(response => {
    console.log('Content-Type:', response.headers.get('Content-Type'));
    console.log('Server:', response.headers.get('Server'));
    // Можно перебрать все заголовки
    for (let [key, value] of response.headers) {
      console.log(`${key}: ${value}`);
    }
    return response.json();
  })
  .then(data => console.log(data));

Важно: некоторые заголовки (например, Set-Cookie) недоступны через JavaScript по соображениям безопасности.

Как выполнить запрос с разными опциями (redirect, referrer, integrity)?

Fetch поддерживает множество дополнительных параметров:

Пример

fetch('https://example.com', {
  // Обработка редиректов: 'follow' (по умолчанию), 'error', 'manual'
  redirect: 'manual',
  // Реферер
  referrer: 'https://mysite.com',
  // Проверка целостности (SRI)
  integrity: 'sha384-...',
  // Таймаут (с помощью AbortController, как описано выше)
})
  .then(response => {
    console.log('Статус:', response.status);
    // При redirect: 'manual' редирект не будет выполнен автоматически
    if (response.status === 301 || response.status === 302) {
      console.log('Редирект на:', response.headers.get('Location'));
    }
  });

Параметр redirect: 'manual' полезен, когда нужно перехватить редирект перед его выполнением, например, для отладки.

Как использовать Fetch с Service Workers?

В Service Worker fetch используется для перехвата сетевых запросов. Пример кэширования ресурсов:

Пример

// В файле service-worker.js
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(cachedResponse => {
        // Вернуть из кэша, если есть, или запросить по сети
        return cachedResponse || fetch(event.request);
      })
  );
});

Этот код позволяет реализовать офлайн-режим. Внутри обработчика fetch можно модифицировать запрос, подменять URL, добавлять заголовки и т.д.

Fetch API в JavaScript - comments

En
Fetch js (javascript)