JS. Начало #8. Fetch

Автор: Рябкова Анна, Коптикова Лилия, Манюшкина Дарья

Fetch API — это интерфейс JavaScript для работы с HTTP-запросами и ответами. Он предоставляет глобальную функцию fetch(), которая позволяет асинхронно получать ресурсы по сети легко и логично.

Ранее для подобных задач использовался XMLHttpRequest, однако fetch() представляет собой более современную, гибкую и мощную альтернативу. Благодаря использованию Promise, этот API проще и удобнее в использовании, особенно при работе с асинхронным кодом и взаимодействии с API.

Основные преимущества Fetch API:

  • Простота использования;
  • Поддержка промисов;
  • Читаемый и понятный синтаксис;
  • Возможность работы с различными типами данных (JSON, текст, Blob и т.д.);
  • Поддержка современных стандартов и технологий.

Основы использования fetch

Синтаксис fetch(url)

fetch(url, [options])

Функция fetch() принимает два параметра:

  • url — адрес, по которому нужно сделать запрос;
  • options (необязательный) — объект с дополнительными настройками запроса

По умолчанию вызов fetch() делает GET-запрос по указанному адресу.

Функция fetch() возвращает объект Promise, который получает ответ после завершения запроса к сетевому ресурсу.

Как получить ответ от сервера

Браузер сразу же начинает запрос и возвращает промис, который внешний код использует для получения результата. Процесс получения ответа обычно происходит в два этапа.

Promise выполняется с объектом встроенного класса Response в качестве результата, как только сервер пришлёт заголовки ответа. На этом этапе можно проверить статус HTTP-запроса и определить, выполнился ли он успешно, а также посмотреть заголовки, но пока без тела ответа.

Если запрос не удался (например, при ошибке сети или отсутствующем домене), промис перейдёт в состояние rejected. HTTP-статусы 404 и 500 не считаются ошибкой на уровне fetch.

Примеры свойств объекта Response:

  • status — HTTP-код ответа, например 200;
  • ok — булево значение, true, если статус от 200 до 299.

Работа с response.json() и response.text()

Для получения тела ответа нужно использовать дополнительный вызов метода. Response предоставляет несколько методов, основанных на промисах, для доступа к телу ответа в различных форматах:

  • response.text() – читает ответ и возвращает как обычный текст,
  • response.json() – декодирует ответ в формате JSON,
  • response.formData() – возвращает ответ как объект FormData,
  • response.blob() – возвращает объект как Blob (бинарные данные с типом),
  • response.arrayBuffer() – возвращает ответ как ArrayBuffer (низкоуровневое представление бинарных данных),

помимо этого, response.body – это объект ReadableStream, с помощью которого можно считывать тело запроса по частям.

Можно выбрать только один метод чтения ответа. Если ответ уже был получен с response.text(), тогда response.json() не сработает, так как данные уже были обработаны.

let text = await response.text(); // тело ответа обработано 
let parsed = await response.json(); // ошибка (данные уже были обработаны)

Пример базового запроса

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

Этот пример демонстрирует, как легко можно выполнить HTTP-запрос и обработать ответ.

Обработка ошибок в fetch

Любой ответ на запрос через fetch(), например, HTTP-код 400, 404 или 500, переводит Promise в состояние fulfilled. Promise перейдёт в состояние rejected только если:

  • случился сбой сети,
  • домен не существует,
  • запрос был заблокирован.

Использование catch для перехвата ошибок

Fetch API возвращает промис, следовательно, можно использовать then, catch и finally. Оборачивание запроса в try/catch — очень распространённый способ обработки ошибок, но не все ошибки можно перехватить.

try { 
  // сервер вернёт ошибку CORS 
  const response = await fetch('https://google.com/api') 
} catch { 
  console.error('Failed') 
} 
// Вывод в консоли: Failed

Этот код попытается выполнить выборку и обнаружить ошибки только в том случае, если Promise в состоянии rejected

Проверка response.ok перед обработкой данных

Другой способ обработки ошибок — проверка статуса ответа при успешном выполнении промиса.

Чтобы обработать ошибку запроса, необходимо обращать внимание на поле ok в объекте ответа Response. В случае ошибки запроса оно будет равно false.

const response = await fetch('https://restcountries.com/v4.1/all') 
if (response.ok) {
   // Обработка ответа 
 } else { 
   console.error('Failed') 
 }

Комбинированная обработка: response.ok + catch

try/catch и response.ok используются для обнаружения различных типов ошибок, поэтому объединение двух подходов используется для лучшей обработки ошибок:

fetch('https://jsonplaceholder.typicode.com/there-is-no-such-route')
  .then((response) => {
    // Проверяем успешность запроса и выкидываем ошибку
    if (!response.ok) {
      throw new Error('Error occurred!')
    }
    
    return response.json()
   })
   // Теперь попадём сюда, так как выбросили ошибку
   .catch((err) => {
     console.log(err)
    }
)
// Error: Error occurred!

try/catch используется для получения ошибок, когда промис отклонён (проблемы с сетью или CORS).

response.ok используется для обработки ошибок сервера (например, 404 или 500), когда промис разрешён.

Настройки запроса

Метод GET по умолчанию

По умолчанию вызов fetch() делает GET-запрос по указанному адресу. Базовый вызов для получения данных можно записать таким образом:

fetch('http://jsonplaceholder.typicode.com/posts')

Для того чтобы добавить параметры запроса в GET-запрос с помощью JavaScript, используйте объект URLSearchParams:

const params = new URLSearchParams({ search: 'term', limit: '5' }).toString();
fetch(`https://api.example.com/items?${params}`)
  .then(res => res.json())
  .then(data => console.log(data));

Этот код позволяет быстро сформировать строку запроса с нужными параметрами и осуществить запрос.

Отправка данных с POST и пример отправки JSON-данных

Для отправки POST-запроса или запроса с другим методом, нам необходимо использовать fetch параметры:

  • method – HTTP метод, например POST,
  • body – тело запроса, одно из списка:
  • строка (например, в формате JSON),
  • объект FormData для отправки данных как form/multipart,
  • Blob/BufferSource для отправки бинарных данных,
  • URLSearchParams для отправки данных в кодировке x-www-form-urlencoded, используется редко.

Чаще всего используется JSON.

Например, этот код отправляет объект user как JSON:

let user = {

  name: 'John',

  surname: 'Smith'

};

let response = await fetch('/article/fetch/post/user', {

  method: 'POST',

  headers: {

    'Content-Type': 'application/json;charset=utf-8'

  },

  body: JSON.stringify(user)

});

let result = await response.json();

alert(result.message);

Заметим, что так как тело запроса body – строка, то заголовок Content-Type по умолчанию будет text/plain;charset=UTF-8.

Но, так как мы посылаем JSON, то используем параметр headers для отправки вместо этого application/json, правильный Content-Type для JSON.

Настройки заголовков через headers

Заголовки ответа

Заголовки ответа хранятся в похожем на Map объекте response.headers.

Это не совсем Map, но мы можем использовать такие же методы, как с Map, чтобы получить заголовок по его имени или перебрать заголовки в цикле:

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

// получить один заголовок

alert(response.headers.get('Content-Type')); // application/json; charset=utf-8

// перебрать все заголовки

for (let [key, value] of response.headers) {

  alert(`${key} = ${value}`);

}

Заголовки запроса

Для установки заголовка запроса в fetch мы можем использовать опцию headers. Она содержит объект с исходящими заголовками, например:

let response = fetch(protectedUrl, {
  headers: {
    Authentication: 'secret'
  }
});

Есть список запрещённых HTTP-заголовков, которые мы не можем установить:

  • Accept-Charset, Accept-Encoding
  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Connection
  • Content-Length
  • Cookie, Cookie2
  • Date
  • DNT
  • Expect
  • Host
  • Keep-Alive
  • Origin
  • Referer
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade
  • Via
  • Proxy-*
  • Sec-*

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

Асинхронный fetch с async/await

async/await

Существует специальный синтаксис для работы с промисами, который называется «async/await».

Начнём с ключевого слова async. Оно ставится перед функцией, вот так:

async function f() {
  return 1;
}

У слова async один простой смысл: эта функция всегда возвращает промис. Значения других типов оборачиваются в завершившийся успешно промис автоматически.

Например, эта функция возвратит выполненный промис с результатом 1:

async function f() {
  return 1;
}
f().then(alert); // 1

Можно и явно вернуть промис, результат будет одинаковым:

async function f() {
  return Promise.resolve(1);
}
f().then(alert); // 1

Так что ключевое слово async перед функцией гарантирует, что эта функция в любом случае вернёт промис. Согласитесь, достаточно просто? Но это ещё не всё. Есть другое ключевое слово – await, которое можно использовать только внутри async-функций.

Await

Синтаксис:

// работает только внутри async–функций
let value = await promise;

Ключевое слово await заставит интерпретатор JavaScript ждать до тех пор, пока промис справа от await не выполнится. После чего оно вернёт его результат, и выполнение кода продолжится.

В этом примере промис успешно выполнится через 1 секунду:

async function f() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("готово!"), 1000)
  });
  let result = await promise; // будет ждать, пока промис не выполнится (*)
  alert(result); // "готово!"
}
f();

В данном примере выполнение функции остановится на строке (*) до тех пор, пока промис не выполнится. Это произойдёт через секунду после запуска функции. После чего в переменную result будет записан результат выполнения промиса, и браузер отобразит alert-окно «готово!».

Обратите внимание, хотя await и заставляет JavaScript дожидаться выполнения промиса, это не отнимает ресурсов процессора. Пока промис не выполнится, JS-движок может заниматься другими задачами: выполнять прочие скрипты, обрабатывать события и т.п.

Пример функции showAvatar() с async/await:

async function showAvatar() {

  // запрашиваем JSON с данными пользователя

  let response = await fetch('/article/promise-chaining/user.json');

  let user = await response.json();

  // запрашиваем информацию об этом пользователе из github

  let githubResponse = await fetch(`https://api.github.com/users/${user.name}`);

  let githubUser = await githubResponse.json();

  // отображаем аватар пользователя

  let img = document.createElement('img');

  img.src = githubUser.avatar_url;

  img.className = "promise-avatar-example";

  document.body.append(img);

  // ждём 3 секунды и затем скрываем аватар

  await new Promise((resolve, reject) => setTimeout(resolve, 3000));

  img.remove();

  return githubUser;
}

showAvatar();

Обработка ошибок с try/catch

Когда промис завершается успешно, await promise возвращает результат. Когда завершается с ошибкой – будет выброшено исключение. Как если бы на этом месте находилось выражение throw.

Такой код:

 async function f() {
  await Promise.reject(new Error("Упс!"));
}

Делает то же самое, что и такой:

async function f() {
  throw new Error("Упс!");
}

Но есть отличие: на практике промис может завершиться с ошибкой не сразу, а через некоторое время. В этом случае будет задержка, а затем await выбросит исключение.

Такие ошибки можно ловить, используя try..catch, как с обычным throw:

async function f() {
  try {
    let response = await fetch('http://no-such-url')
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}
f();

В случае ошибки выполнение try прерывается и управление прыгает в начало блока catch. Блоком try можно обернуть несколько строк:

async function f() {
  try {
    let response = await fetch('/no-user-here');
    let user = await response.json();
  } catch(err) {
    // перехватит любую ошибку в блоке try: и в fetch, и в response.json
    alert(err);
  }
}
f();

Если у нас нет try..catch, асинхронная функция будет возвращать завершившийся с ошибкой промис (в состоянии rejected). В этом случае мы можем использовать метод .catch промиса, чтобы обработать ошибку:

async function f() {
  let response = await fetch('http://no-such-url');
}
// f() вернёт промис в состоянии rejected
f().catch(alert); // TypeError: failed to fetch // (*)

Если забыть добавить .catch, то будет сгенерирована ошибка «Uncaught promise error» и информация об этом будет выведена в консоль. Такие ошибки можно поймать глобальным обработчиком.

Когда использовать await fetch()

Ключевое слово async перед объявлением функции:

  1. Обязывает её всегда возвращать промис.
  2. Позволяет использовать await в теле этой функции.

Ключевое слово await перед промисом заставит JavaScript дождаться его выполнения, после чего:

  1. Если промис завершается с ошибкой, будет сгенерировано исключение, как если бы на этом месте находилось throw.
  2. Иначе вернётся результат промиса.

Вместе они предоставляют отличный каркас для написания асинхронного кода. Такой код легко и писать, и читать.

Хотя при работе с async/await можно обходиться без promise.then/catch, иногда всё-таки приходится использовать эти методы (на верхнем уровне вложенности, например). Также await отлично работает в сочетании с Promise.all, если необходимо выполнить несколько задач параллельно.

Работа с несколькими запросами

Как запрашивать данные параллельно с Promise.all

Метод all() — это один из статических методов объекта Promise. Метод all() используют, когда нужно запустить несколько промисов параллельно и дождаться их выполнения.

Promise.all() принимает итерируемую коллекцию промисов (чаще всего — массив) и возвращает новый промис, который будет выполнен, когда будут выполнены все промисы, переданные в виде перечисляемого аргумента, или отклонён, если хотя бы один из переданных промисов завершится с ошибкой.

Метод Promise.all() возвращает массив значений всех переданных промисов, при этом сохраняя порядок оригинального (переданного) массива, но не порядок выполнения.

Создадим несколько промисов:

const promise1 = new Promise(resolve => setTimeout(() => resolve(1), 5000))

const promise2 = new Promise(resolve => setTimeout(() => resolve(2), 2000))

const promise3 = new Promise(resolve => setTimeout(() => resolve(3), 1000))

Передадим массив из созданных промисов в Promise.all():

Promise.all([promise1, promise2, promise3])
  .then(([response1, response2, response3]) => {
    console.log(response1)
    // 1
    
    console.log(response2)
    // 2

    console.log(response3)
    // 3

  })

Если передать пустой массив, то Promise.all() будет выполнен немедленно.

Если хотя бы один промис из переданного массива завершится с ошибкой, то Promise.all() тоже завершится с этой ошибкой. Метод уже не будет следить за выполнением оставшихся промисов, которые рано или поздно все-таки выполнятся, и их результаты будут просто проигнорированы.

В примере ниже, промис promise2 завершается с ошибкой:

const promise1 = new Promise(
  resolve => setTimeout(() => resolve(1), 5000)
)

const promise2 = new Promise(
  (resolve, reject) => setTimeout(() => reject('error'), 2000)
)

const promise3 = new Promise(
  resolve => setTimeout(() => resolve(3), 1000)

)

Promise.all([promise1, promise2, promise3])
  .then(([response1, response2, response3 ]) => {
    console.log(response1)
    console.log(response2)
    console.log(response3)
  })

  .catch(error => {
    console.error(error)
    // error
  })

В итоге обработчик then()будет проигнорирован, и будет выполняться код из обработчика ошибок catch().

Пример загрузки нескольких API одновременно

В веб-разработке часто возникает задача: получить данные сразу с нескольких внешних источников - API. Чтобы ускорить выполнение и не блокировать интерфейс, лучше загружать эти данные параллельно.

Параллельные запросы позволяют уменьшить общее время ожидания ответа, не блокировать интерфейс пользователя и рационально использовать возможности сети.

Пример кода:

const urls = [
  "https://jsonplaceholder.typicode.com/posts/1",
  "https://jsonplaceholder.typicode.com/users/1"
];

async function loadData() {
  try {
    const responses = await Promise.all(urls.map(url => fetch(url)));
    const data = await Promise.all(responses.map(r => r.json()));
    console.log("Результаты:", data);
  } catch (error) {
    console.error("Ошибка при загрузке:", error);
  }
}
loadData();

Этот код отправляет два запросы параллельно с помощью urls.map(fetch), затем ожидает, пока оба вернут ответ (Promise.all(…)). После чего преобразует ответы в JSON формат и выведет результаты в консоль.

Как делать последовательные запросы

Последовательные запросы - ситуация, когда второй запрос начинается только после завершения первого. Для этого в JavaScript используется await последовательно внутри async функции.

async function fetchSequentially() {
  try {
    const response1 = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    const data1 = await response1.json();
    console.log("Первый ответ:", data1);
    
    const response2 = await fetch("https://jsonplaceholder.typicode.com/users/" + data1.userId);
    const data2 = await response2.json();
    console.log("Второй ответ:", data2);
    
  } catch (error) {
    console.error("Произошла ошибка:", error);
  }
}

fetchSequentially();

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

  • Когда второй запрос зависит от данных первого;
  • Когда нужно избегать перегрузки API;
  • Когда важно соблюдать порядок операций.

Обработка статусов ответа

Работа с кодами ответа (200, 404, 500)

Работа с кодами ответа (200, 404, 500 и т. д.) в JavaScript делается через объект Response, который возвращает fetch. У него есть поле .status, где хранится HTTP-код.

Пример кода:

async function fetchWithStatusCheck(url) {
  try {
    const response = await fetch(url);

    // Проверка кода ответа

    if (response.status === 200) {
      const data = await response.json();
      console.log("Успешно:", data);
      
    } else if (response.status === 404) {
      console.warn("Не найдено (404):", url);
      
    } else if (response.status >= 500) {
      console.error("Ошибка сервера:", response.status);
   
    } else {
      console.log("Другой статус:", response.status);
    }

  } catch (error) {
    console.error("Ошибка сети или fetch:", error);
  }
}

fetchWithStatusCheck("https://jsonplaceholder.typicode.com/posts/1"); // 200

fetchWithStatusCheck("https://jsonplaceholder.typicode.com/unknown"); // 404

Как правильно обрабатывать ошибки API

Обработка ошибок нужна в следующих случаях:

  • Сетевая ошибка - когда нет интернета, API-сервер недоступен, таймаут, DNS-ошибка и т.п.
  • API вернул ошибку (404, 500 и др.) - запрос выполнен, но сервер сообщил об ошибке
  • Ошибка парсинга JSON - сервер вернул невалидный JSON или другой тип ответа

Пример кода с обработкой ошибок и с использованием fetch

async function loadData() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    if (!response.ok) {
      throw new Error(`Ошибка: ${response.status}`);
    }
    const data = await response.json();
    console.log("Данные:", data);
  } catch (error) {
    console.error("Произошла ошибка:", error.message);
  }
}
loadData();

Пример использования fetch на практике

Запрос списка пользователей с JSONPlaceholder

JSONPlaceholder — это бесплатный фейковый онлайн-API, который предоставляет данные, такие как список пользователей, посты, комментарии и другие ресурсы. Это отличный инструмент для тестирования и обучения работе с API.

Для выполнения запросов к API мы используем fetch — встроенную функцию в JavaScript для выполнения HTTP-запросов. Мы будем работать с методом GET, чтобы получить список пользователей.

async function getUsers() {
  try {
    // Отправляем запрос на сервер
    const response = await fetch("https://jsonplaceholder.typicode.com/users");

    // Проверяем успешность ответа (код 200)
    if (!response.ok) {
      throw new Error(`Ошибка: ${response.status}`);
    }

    // Преобразуем ответ в формат JSON
    const users = await response.json();

    // Выводим список пользователей в консоль
    console.log("Список пользователей:", users);

  } catch (error) {
    // Обработка ошибок (например, нет интернета или ошибка API)
    console.error("Ошибка при загрузке пользователей:", error.message);
  }
}

// Вызываем функцию для получения данных
getUsers();

Когда вы вызываете fetch(“https://jsonplaceholder.typicode.com/users”), выполняется запрос к серверу. Он будет асинхронным, то есть не заблокирует выполнение других операций в программе.

Отправка формы через fetch

Отправка данных формы через fetch — это современный способ отправки запросов с помощью JavaScript. Он позволяет делать это асинхронно, без необходимости перезагружать страницу. Это удобно для создания динамичных веб-приложений, где форма отправляется в фоновом режиме.

Предположим, у нас есть простая форма, в которой пользователь вводит имя и email, и мы хотим отправить эти данные на сервер.

document.getElementById('userForm').addEventListener('submit', async function(event) {

  event.preventDefault(); // Предотвращаем стандартную отправку формы
  // Сбор данных из формы
  const formData = new FormData(this);
  try {
    // Отправляем данные формы с помощью fetch
    const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',  // Метод запроса
      body: formData   // Данные формы
    });
    
    // Проверка успешности ответа
    if (!response.ok) {
      throw new Error(`Ошибка: ${response.status}`);
    }
    
    // Получаем и выводим ответ сервера
    const data = await response.json();
    console.log('Ответ сервера:', data);
    
    // Сообщение о успешной отправке
    alert('Форма отправлена успешно!');
    
  } catch (error) {
    console.error('Ошибка при отправке формы:', error.message);
    alert('Ошибка при отправке формы');
  }
});

Вешаем обработчик на событие отправки формы. С помощью event.preventDefault() предотвращаем стандартное поведение формы (перезагрузку страницы). Для сбора данных используем FormData. Этот объект автоматически собирает все данные из формы, включая файлы и текстовые поля. Мы отправляем запрос с методом POST на сервер. В поле body указываем данные формы, которые автоматически сериализуются в нужный формат для отправки. После отправки мы проверяем, был ли запрос успешным, используя response.ok. Если запрос неудачный (например, сервер вернул код 404 или 500), выбрасывается ошибка. Если что-то пошло не так — мы ловим ошибку с помощью catch и выводим соответствующее сообщение.

Рябкова Анна, Коптикова Лилия, Манюшкина Дарья

Рябкова Анна, Коптикова Лилия, Манюшкина Дарья