Клавиша / esc

fetch()

Модно и современно отправляем запросы на сервер.

Время чтения: 8 мин

Кратко

Скопировано

С помощью глобальной функции fetch() можно выполнять HTTP-запросы — как получать, так и отправлять данные. Функция возвращает промис, который резолвится в объект ответа, где находится дополнительная информация (статус ответа, заголовки) и ответ на запрос.

Пример

Скопировано

Рассмотрим несколько примеров использования fetch().

Проверим доступ к тестовому API с помощью GET-запроса:

        
          
          fetch('https://dummyjson.com/test')  .then(response => response.json())  .then(data => console.log(data))  .catch(error => console.error(error.message))// { status: 'ok', method: 'GET' }
          fetch('https://dummyjson.com/test')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error.message))

// { status: 'ok', method: 'GET' }

        
        
          
        
      

А вот такой же запрос с синтаксисом async/await:

        
          
          try {  const response = await fetch('https://dummyjson.com/test')  const data = await response.json()  console.log(data)} catch (error) {  console.error(error.message)}// { status: 'ok', method: 'GET' }
          try {
  const response = await fetch('https://dummyjson.com/test')
  const data = await response.json()
  console.log(data)
} catch (error) {
  console.error(error.message)
}
// { status: 'ok', method: 'GET' }

        
        
          
        
      

Отправим данные с помощью POST-запроса:

        
          
          try {  const response = await fetch('https://dummyjson.com/todos/add', {    method: 'POST',    headers: { 'Content-Type': 'application/json' },    body: JSON.stringify({      todo: 'Выучить С++ за 21 день',      userId: '42',      completed: false    })  })  const data = await response.json()  console.log(data)} catch (error) {  console.error(error.message)}// {//   id: 255,//   todo: 'Выучить С++ за 21 день',//   completed: false,//   userId: '42'// }
          try {
  const response = await fetch('https://dummyjson.com/todos/add', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      todo: 'Выучить С++ за 21 день',
      userId: '42',
      completed: false
    })
  })
  const data = await response.json()
  console.log(data)
} catch (error) {
  console.error(error.message)
}
// {
//   id: 255,
//   todo: 'Выучить С++ за 21 день',
//   completed: false,
//   userId: '42'
// }

        
        
          
        
      

Как пишется

Скопировано
        
          
          fetch(resource, options)
          fetch(resource, options)

        
        
          
        
      

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

  • resource — строка, содержащая URL-адрес, по которому нужно сделать запрос, или специальный Request-объект, хранящий данные запроса;
  • options (необязательный) — объект конфигурации, в котором можно настроить метод и тело запроса, заголовки и многое другое.

Для выполнения GET-запроса к серверу по url-адресу можно использовать краткий синтаксис: fetch(url).

Функция fetch() возвращает Promise, который:

  • при успешном выполнении запроса резолвится в объект Response, представляющий ответ от сервера;
  • будет отклонён (rejected) при возникновении сетевых ошибок (например: нет доступа к сети, CORS‑блокировка).

При вызове fetch() будет брошено исключение TypeError если:

  • указан некорректный URL-адрес;
  • объект конфигурации options содержит недопустимые параметры;

Как понять

Скопировано

Функция fetch() предназначена для выполнения HTTP-запросов. Раньше для этой цели использовался XMLHttpRequest, однако fetch() более гибкое и универсальное решение.

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

Гибкость интерфейса fetch() основана на использовании специальных объектов:

  • Request, хранит информацию о запросе (URL-адрес, HTTP-метод, формат передаваемых данных, данные и заголовки запроса );
  • Response, содержит данные о результате и методы обработки ответа.

Глобальная функция fetch() не описывается спецификацией ECMAScript и доступна в браузере как часть Web API, а вне браузера (Node.js, Deno, Bun) как часть runtime API.

Формирование и отправка запроса

Скопировано

Указать HTTP-метод, добавить заголовки и тело запроса, можно c помощью объекта options:

        
          
          // Данные для отправки на серверconst newPost = {  title: 'foo',  userId: 1,}fetch('https://jsonplaceholder.typicode.com/posts', {  method: 'POST', // Здесь так же могут быть GET, PUT, DELETE  // Тело запроса в JSON-формате  body: JSON.stringify(newPost),  headers: {    // Добавляем необходимые заголовки    'Content-type': 'application/json; charset=UTF-8',  }})  .then((response) => response.json())  .then((data) => {    console.log(data)    // {title: "foo", userId: 1, id: 101}  })
          // Данные для отправки на сервер
const newPost = {
  title: 'foo',
  userId: 1,
}

fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST', // Здесь так же могут быть GET, PUT, DELETE
  // Тело запроса в JSON-формате
  body: JSON.stringify(newPost),
  headers: {
    // Добавляем необходимые заголовки
    'Content-type': 'application/json; charset=UTF-8',
  }
})
  .then((response) => response.json())
  .then((data) => {
    console.log(data)
    // {title: "foo", userId: 1, id: 101}
  }
)

        
        
          
        
      

Выполним тот же запрос, но с явным созданием объекта Request, принимающего те же параметры, что и fetch():

        
          
          const newPost = {  title: 'foo',  userId: 1,}// Создаём объект Requestconst request = new Request(  'https://jsonplaceholder.typicode.com/posts', {  method: 'POST',  body: JSON.stringify(newPost),  headers: {    'Content-type': 'application/json; charset=UTF-8',  }})// Выполняем запросfetch(request)  .then((response) => response.json())  .then((data) => {    console.log(data)    // {title: "foo", userId: 1, id: 101}  })
          const newPost = {
  title: 'foo',
  userId: 1,
}

// Создаём объект Request
const request = new Request(
  'https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  body: JSON.stringify(newPost),
  headers: {
    'Content-type': 'application/json; charset=UTF-8',
  }
})

// Выполняем запрос
fetch(request)
  .then((response) => response.json())
  .then((data) => {
    console.log(data)
    // {title: "foo", userId: 1, id: 101}
  }
)

        
        
          
        
      

Объект Request помогает разделять логику формирования и выполнения запросов, позволяя динамически формировать и изменять параметры запроса.

Если передать в fetch() оба параметра, то можно изменить параметры запроса при отправке:

        
          
          // Создаём объект Request c данными { title: 'bar', userId: 123 }const request = new Request(  'https://jsonplaceholder.typicode.com/posts', {  method: 'POST',  body: JSON.stringify({    title: 'bar',    userId: 123,  }),  headers: {    'Content-type': 'application/json; charset=UTF-8',  }})fetch(request, {  // Передаём в options данные { title: 'baz', userId: 42 }  body: JSON.stringify({    title: 'baz',    userId: 42,  }),})  .then((response) => response.json())  .then((data) => {    console.log(data)    // {title: "baz", userId: 42, id: 101}  })
          // Создаём объект Request c данными { title: 'bar', userId: 123 }
const request = new Request(
  'https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  body: JSON.stringify({
    title: 'bar',
    userId: 123,
  }),
  headers: {
    'Content-type': 'application/json; charset=UTF-8',
  }
})

fetch(request, {
  // Передаём в options данные { title: 'baz', userId: 42 }
  body: JSON.stringify({
    title: 'baz',
    userId: 42,
  }),
})
  .then((response) => response.json())
  .then((data) => {
    console.log(data)
    // {title: "baz", userId: 42, id: 101}
  }
)

        
        
          
        
      

Получение ответа

Скопировано

Результатом вызова fetch() будет промис. При завершении запроса промис резолвится в объект ответа Response, который содержит информацию об ответе сервера. У этого объекта есть множество полезных полей и методов. Вот основные:

  • ok — принимает состояние true или false и сообщает об успешности запроса;
  • status — содержит HTTP-код ответа, например: 200, 400, 404;
  • headers — объект заголовков ответа Headers;
  • json() — возвращает результат запроса в виде JSON;
  • text() — возвращает результат запроса в виде текста;
  • blob() — возвращает результат запроса в виде объекта файла;
  • arrayBuffer() — возвращает результат запроса в виде бинарных данных.

В следующем примере используем .then() для обработки результата, полученного от асинхронной операции. Обработчик принимает ответ сервера и выполняет с помощью метода json() преобразование тела ответа в объект JavaScript. Результат вызова json() это тоже промис, поэтому отображение полученных данных будет выполнено тоже асинхронно следующим колбэком в цепочке.

        
          
          fetch('http://jsonplaceholder.typicode.com/posts')  .then((response) => response.json())  .then(data => console.log(data))  // [{...}, {...}, {...}, ...]
          fetch('http://jsonplaceholder.typicode.com/posts')
  .then((response) => response.json())
  .then(data => console.log(data))
  // [{...}, {...}, {...}, ...]

        
        
          
        
      

Cookies

Скопировано

По умолчанию fetch() запросы не включают в себя cookies, и поэтому авторизованные запросы на сервере могут не пройти. Для этого необходимо добавить в настройку поле credentials:

        
          
          fetch('https://somesite.com/admin', {  method: 'GET',  // Или same-origin, если можно делать такие запросы  // только в пределах этого домена  credentials: 'include',})
          fetch('https://somesite.com/admin', {
  method: 'GET',
  // Или same-origin, если можно делать такие запросы
  // только в пределах этого домена
  credentials: 'include',
})

        
        
          
        
      

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

Скопировано

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

        
          
          // Запрос вернёт ошибку «404 Not Found»fetch('https://jsonplaceholder.typicode.com/there-is-no-such-route').catch(  () => {    console.log('Error occurred!')  })// Никогда не выполнится
          // Запрос вернёт ошибку «404 Not Found»
fetch('https://jsonplaceholder.typicode.com/there-is-no-such-route').catch(
  () => {
    console.log('Error occurred!')
  }
)
// Никогда не выполнится

        
        
          
        
      

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

        
          
          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!
          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!

        
        
          
        
      

В большинстве случаев ошибки, возникающие при выполнении fetch(), асинхронны: промис переходит в состояние rejected. Такие ошибки можно обработать с помощью .catch(). При этом при подготовке данных запроса может возникнуть и синхронная ошибка, например:

        
          
          try {  // Объект с циклической ссылкой  const invalidBody = { key: "value" }  invalidBody.self = invalidBody  fetch('https://dummyjson.com/posts/add', {    method: 'POST',    headers: { 'Content-Type': 'application/json' },    // Пытаемся преобразовать объект в JSON    // Здесь возникнет синхронная ошибка    body: JSON.stringify(invalidBody)  })    .then(response => response.json())    .then(data => console.log(data))    .catch(error => console.log('Асинхронная ошибка:', error))} catch (error) {  // Ловим синхронную ошибку  console.log('Синхронная ошибка:', error.message)}// Синхронная ошибка: Converting circular structure to JSON
          try {
  // Объект с циклической ссылкой
  const invalidBody = { key: "value" }
  invalidBody.self = invalidBody

  fetch('https://dummyjson.com/posts/add', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    // Пытаемся преобразовать объект в JSON
    // Здесь возникнет синхронная ошибка
    body: JSON.stringify(invalidBody)
  })
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.log('Асинхронная ошибка:', error))
} catch (error) {
  // Ловим синхронную ошибку
  console.log('Синхронная ошибка:', error.message)
}
// Синхронная ошибка: Converting circular structure to JSON

        
        
          
        
      

Оборачивая код подготовки данных и выполнения запроса в try/catch мы можем быть уверены, что любая ошибка будет обработана.

На практике

Скопировано

Егор Огарков советует

Скопировано

Отмена запроса

Скопировано

Иногда может возникнуть необходимость прервать запрос, например, когда авторизация пользователя просрочена или пользователь самостоятельно хочет отменить запрос (отменил скачивание файла).

        
          
          const controller = new AbortController()function fetchData() {  return fetch('http://jsonplaceholder.typicode.com/posts', {    signal: controller.signal,  })    .then((response) => response.json())    .catch((e) => {      console.log(e)    }  )}fetchData()// Если запрос ещё не выполнился, то он будет прерван.// Прерванный fetch вернёт Promise с ошибкойcontroller.abort()
          const controller = new AbortController()

function fetchData() {
  return fetch('http://jsonplaceholder.typicode.com/posts', {
    signal: controller.signal,
  })
    .then((response) => response.json())
    .catch((e) => {
      console.log(e)
    }
  )
}

fetchData()

// Если запрос ещё не выполнился, то он будет прерван.
// Прерванный fetch вернёт Promise с ошибкой
controller.abort()

        
        
          
        
      

Запрос не выполнится, в консоли будет ошибка The user aborted a request. Если заглянуть в инструменты разработчика, то там можно увидеть отменённый статус у запроса.

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

Загрузка файла на сервер

Скопировано

С помощью fetch() можно загружать файлы на сервер, например, когда пользователь хочет загрузить свой аватар в профиль. Отправку файлов можно осуществлять с помощью специального объекта FormData. Покажем на примере обработчика отправки формы:

        
          
          <form id="form">  <input type="file" id="avatar">  <button type="submit">Загрузить</button></form>
          <form id="form">
  <input type="file" id="avatar">
  <button type="submit">Загрузить</button>
</form>

        
        
          
        
      
        
          
          // Находим элемент с файломconst fileInput = document.getElementById('avatar')const form = document.getElementById('form')function handleSubmit(event) {  event.preventDefault()  const formData = new FormData()  // Добавляем файлы из инпута к данным  for (let i = 0; i < fileInput.files.length; i++) {    const file = fileInput.files[i]    formData.append('avatar', file, file.name)  }  // Отправляем файлы на сервер  fetch('https://backend.com/api/upload', {    method: "POST",    body: formData,  })}form.addEventListener('submit', handleSubmit)
          // Находим элемент с файлом
const fileInput = document.getElementById('avatar')
const form = document.getElementById('form')

function handleSubmit(event) {
  event.preventDefault()

  const formData = new FormData()

  // Добавляем файлы из инпута к данным
  for (let i = 0; i < fileInput.files.length; i++) {
    const file = fileInput.files[i]
    formData.append('avatar', file, file.name)
  }

  // Отправляем файлы на сервер
  fetch('https://backend.com/api/upload', {
    method: "POST",
    body: formData,
  })
}

form.addEventListener('submit', handleSubmit)

        
        
          
        
      

Скачивание данных с результатом прогресса

Скопировано

Чтобы получать текущий прогресс скачивания файла или любых других данных, используйте свойство body объекта Response, который возвращается в Promise после вызова fetch(). Поле body является «потоком для чтения» (Readable Stream). Это специальный объект, который даёт возможность получать информацию по частям, по мере её поступления на клиент.

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

        
          
          fetch('https://i.imgur.com/C5QXZ7u.mp4').then(async (response) => {  let received = 0  // Получаем поток в переменную  const reader = response.body.getReader()  // Считываем общую длину данных  const contentLength = parseInt(response.headers.get('Content-Length'), 10)  while (true) {    // После вызова read() возвращается объект, в котором    // done — boolean-значение о том, закончилась ли информация,    // value — массив байт, которые пришли в этот раз    const { done, value } = await reader.read()    if (done) {      console.log('Получено 100%')      break    }    received += (value.length / contentLength) * 100    const result = Math.floor(received)    if (result < 100) {      console.log(`Получено ${result}%`)    }  }})
          fetch('https://i.imgur.com/C5QXZ7u.mp4').then(async (response) => {
  let received = 0

  // Получаем поток в переменную
  const reader = response.body.getReader()

  // Считываем общую длину данных
  const contentLength = parseInt(response.headers.get('Content-Length'), 10)

  while (true) {
    // После вызова read() возвращается объект, в котором
    // done — boolean-значение о том, закончилась ли информация,
    // value — массив байт, которые пришли в этот раз
    const { done, value } = await reader.read()

    if (done) {
      console.log('Получено 100%')
      break
    }

    received += (value.length / contentLength) * 100

    const result = Math.floor(received)

    if (result < 100) {
      console.log(`Получено ${result}%`)
    }
  }
})