Истекло время ожидания запроса API получения?


100

У меня есть fetch-api POSTпросьба:

fetch(url, {
  method: 'POST',
  body: formData,
  credentials: 'include'
})

Я хочу знать, какой тайм-аут по умолчанию для этого? и как мы можем установить его на определенное значение, например 3 секунды или неопределенные секунды?

Ответы:


78

Редактировать 1

Как указано в комментариях, код в исходном ответе продолжает запускать таймер даже после того, как обещание разрешено / отклонено.

Приведенный ниже код устраняет эту проблему.

function timeout(ms, promise) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error('TIMEOUT'))
    }, ms)

    promise
      .then(value => {
        clearTimeout(timer)
        resolve(value)
      })
      .catch(reason => {
        clearTimeout(timer)
        reject(reason)
      })
  })
}


Оригинальный ответ

У него нет указанного по умолчанию; в спецификации вообще не обсуждаются таймауты.

Вы можете реализовать свою собственную оболочку тайм-аута для обещаний в целом:

// Rough implementation. Untested.
function timeout(ms, promise) {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      reject(new Error("timeout"))
    }, ms)
    promise.then(resolve, reject)
  })
}

timeout(1000, fetch('/hello')).then(function(response) {
  // process response
}).catch(function(error) {
  // might be a timeout error
})

Как описано в https://github.com/github/fetch/issues/175 Комментарий от https://github.com/mislav


27
Почему это принятый ответ? SetTimeout здесь будет продолжать работать, даже если обещание разрешится. Лучшим решением было бы сделать это: github.com/github/fetch/issues/175#issuecomment-216791333
radtad

3
@radtad mislav защищает свой подход ниже в этой ветке: github.com/github/fetch/issues/175#issuecomment-284787564 . Не имеет значения, что тайм-аут продолжается, потому что вызов .reject()уже выполненного обещания ничего не делает.
Марк Эмери

1
хотя функция "выборка" отклоняется по таймауту, фоновое tcp-соединение не закрывается. Как я могу корректно завершить процесс моего узла?
Prog Quester

26
СТОП! Это неправильный ответ! Хотя это выглядит хорошим и рабочим решением, но на самом деле соединение не будет закрыто, которое в конечном итоге занимает TCP-соединение (может быть даже бесконечным - зависит от сервера). Представьте, что это НЕПРАВИЛЬНОЕ решение будет реализовано в системе, которая повторно пытается установить соединение каждый период времени - это может привести к сбою (перегрузке) сетевого интерфейса и, в конечном итоге, к зависанию вашей машины! @Endless опубликовал правильный ответ здесь .
Славик Мельцер

1
@SlavikMeltser Я не понимаю. Указанный вами ответ также не нарушает TCP-соединение.
Матеус Пирес,

143

Мне очень нравится чистый подход от этой сущности , используя Promise.race

fetchWithTimeout.js

export default function (url, options, timeout = 7000) {
    return Promise.race([
        fetch(url, options),
        new Promise((_, reject) =>
            setTimeout(() => reject(new Error('timeout')), timeout)
        )
    ]);
}

main.js

import fetch from './fetchWithTimeout'

// call as usual or with timeout as 3rd argument

fetch('http://google.com', options, 5000) // throw after max 5 seconds timeout error
.then((result) => {
    // handle result
})
.catch((e) => {
    // handle errors and timeout error
})

2
Это вызывает «Необработанное отклонение», если fetchошибка происходит по истечении тайм-аута. Это может быть решено обработкой ( .catch) fetchошибки и повторным вызовом, если тайм-аут еще не наступил.
lionello

5
ИМХО это может быть улучшена futher с AbortController при отклонении см stackoverflow.com/a/47250621 .
RiZKiT

Было бы лучше очистить тайм-аут, если выборка также прошла успешно.
Bob9630

105

Используя AbortController , вы сможете сделать это:

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

const fetchPromise = fetch(url, {signal});

// 5 second timeout:
const timeoutId = setTimeout(() => controller.abort(), 5000);


fetchPromise.then(response => {
  // completed request before timeout fired

  // If you only wanted to timeout the request, not the response, add:
  // clearTimeout(timeoutId);
})

14
Это выглядит даже лучше, чем решение для обещания-гонки, потому что оно, вероятно, прерывает запрос, а не просто принимает предыдущий ответ. Поправьте меня если я ошибаюсь.
Карл Адлер

3
Ответ не объясняет, что такое AbortController. Кроме того, он является экспериментальным и должен быть заполнен полифиллами в неподдерживаемых движках, также это не синтаксис.
Estus Flask

Возможно, это не объясняет, что такое AbortController (я добавил ссылку на ответ, чтобы облегчить задачу для ленивых), но это лучший ответ, поскольку он подчеркивает тот факт, что простое игнорирование запроса не означает, что он все еще не ожидается. Отличный ответ.
Аурелио

2
«Я добавил ссылку к ответу, чтобы облегчить задачу для ленивых» - в нем действительно должна быть ссылка и дополнительная информация в соответствии с правилами tbh. Но спасибо за улучшение ответа.
Джей Уик

6
Лучше получить такой ответ, чем ничего не ответить, потому что людей отталкивают придирки, tbh
Майкл Терри

21

Основываясь на отличном ответе Endless , я создал полезную служебную функцию.

const fetchTimeout = (url, ms, { signal, ...options } = {}) => {
    const controller = new AbortController();
    const promise = fetch(url, { signal: controller.signal, ...options });
    if (signal) signal.addEventListener("abort", () => controller.abort());
    const timeout = setTimeout(() => controller.abort(), ms);
    return promise.finally(() => clearTimeout(timeout));
};
  1. Если время ожидания истекло до того, как ресурс был извлечен, выборка прерывается.
  2. Если ресурс извлекается до истечения времени ожидания, время ожидания сбрасывается.
  3. Если входной сигнал прерывается, выборка прерывается, и время ожидания сбрасывается.
const controller = new AbortController();

document.querySelector("button.cancel").addEventListener("click", () => controller.abort());

fetchTimeout("example.json", 5000, { signal: controller.signal })
    .then(response => response.json())
    .then(console.log)
    .catch(error => {
        if (error.name === "AbortError") {
            // fetch aborted either due to timeout or due to user clicking the cancel button
        } else {
            // network error or json parsing error
        }
    });

Надеюсь, это поможет.


9

в API выборки пока нет поддержки тайм-аута. Но этого можно достичь, обернув его обещанием.

например, для

  function fetchWrapper(url, options, timeout) {
    return new Promise((resolve, reject) => {
      fetch(url, options).then(resolve, reject);

      if (timeout) {
        const e = new Error("Connection timed out");
        setTimeout(reject, timeout, e);
      }
    });
  }

мне больше нравится этот, менее повторяющийся, чтобы использовать более одного раза.
dandavis

1
Запрос здесь не отменяется по истечении таймаута, правильно? Это может быть хорошо для OP, но иногда вы хотите отменить запрос на стороне клиента.
trysis 01

2
@trysis ну да. Недавно реализовано решение для прерывания выборки с помощью AbortController , но все еще экспериментальное с ограниченной поддержкой браузера. Обсуждение
code-jaff 03

Забавно, IE и Edge - единственные, кто это поддерживает! Если мобильный сайт Mozilla снова не
сработает

Firefox поддерживает его с 57 года. :: смотрит в Chrome ::
Франклин Ю

7

РЕДАКТИРОВАТЬ : запрос на выборку по-прежнему будет выполняться в фоновом режиме и, скорее всего, будет регистрировать ошибку в вашей консоли.

Действительно Promise.raceподход лучше.

См. Эту ссылку для справки Promise.race ()

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

fetchWithTimeout(url, {
  method: 'POST',
  body: formData,
  credentials: 'include',
}, 5000, () => { /* do stuff here */ });

Если это вас заинтересует, возможная реализация:

function fetchWithTimeout(url, options, delay, onTimeout) {
  const timer = new Promise((resolve) => {
    setTimeout(resolve, delay, {
      timeout: true,
    });
  });
  return Promise.race([
    fetch(url, options),
    timer
  ]).then(response => {
    if (response.timeout) {
      onTimeout();
    }
    return response;
  });
}

2

Вы можете создать оболочку timeoutPromise

function timeoutPromise(timeout, err, promise) {
  return new Promise(function(resolve,reject) {
    promise.then(resolve,reject);
    setTimeout(reject.bind(null,err), timeout);
  });
}

Затем вы можете обернуть любое обещание

timeoutPromise(100, new Error('Timed Out!'), fetch(...))
  .then(...)
  .catch(...)  

На самом деле это не отменяет базовое соединение, но позволит вам отсрочить выполнение обещания.
Справка


2

Если вы не настроили тайм-аут в своем коде, это будет тайм-аут запроса по умолчанию для вашего браузера.

1) Firefox - 90 секунд

Введите about:configURL-адрес Firefox. Найдите значение, соответствующее ключуnetwork.http.connection-timeout

2) Хром - 300 секунд

Источник



0

При использовании c-обещания2 lib отменяемая выборка с тайм-аутом может выглядеть так ( живая демонстрация jsfiddle ):

import CPromise from "c-promise2"; // npm package

function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) {
    return new CPromise((resolve, reject, {signal}) => {
        fetch(url, {...fetchOptions, signal}).then(resolve, reject)
    }, timeout)
}
        
const chain = fetchWithTimeout("https://run.mocky.io/v3/753aa609-65ae-4109-8f83-9cfe365290f0?mocky-delay=10s", {timeout: 5000})
    .then(request=> console.log('done'));
    
// chain.cancel(); - to abort the request before the timeout

Этот код как пакет npm cp-fetch

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.