Pobrać limit czasu żądania API?


107

Mam fetch-api POSTprośbę:

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

Chcę wiedzieć, jaki jest domyślny limit czasu w tym przypadku? i jak możemy ustawić ją na określoną wartość, taką jak 3 sekundy lub nieokreślone sekundy?

Odpowiedzi:


78

Edytuj 1

Jak wskazano w komentarzach, kod w oryginalnej odpowiedzi nadal uruchamia licznik czasu nawet po rozwiązaniu / odrzuceniu obietnicy.

Poniższy kod rozwiązuje ten problem.

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)
      })
  })
}


Oryginalna odpowiedź

Nie ma określonej wartości domyślnej; specyfikacja w ogóle nie omawia limitów czasu.

Możesz zaimplementować własne opakowanie limitu czasu dla obietnic w ogóle:

// 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
})

Jak opisano w https://github.com/github/fetch/issues/175 Komentarz https://github.com/mislav


28
Dlaczego jest to akceptowana odpowiedź? SetTimeout tutaj będzie działać, nawet jeśli obietnica zostanie rozwiązana. Lepszym rozwiązaniem byłoby zrobienie tego: github.com/github/fetch/issues/175#issuecomment-216791333
radtad

3
@radtad mislav broni swojego podejścia niżej w tym wątku: github.com/github/fetch/issues/175#issuecomment-284787564 . Nie ma znaczenia, że ​​limit czasu trwa, ponieważ odwołanie .reject()się do już rozwiązanej obietnicy nic nie daje.
Mark Amery,

1
chociaż funkcja „fetch” jest odrzucana przez przekroczenie limitu czasu, połączenie TCP w tle nie jest zamykane. Jak mogę bezpiecznie zakończyć proces węzła?
Prog Quester

28
ZATRZYMAĆ! To jest nieprawidłowa odpowiedź! Wprawdzie wygląda to na dobre i działające rozwiązanie, ale w rzeczywistości połączenie nie zostanie zamknięte, co ostatecznie zajmie połączenie TCP (może być nawet nieskończone - zależy od serwera). Wyobraź sobie, że to NIEPRAWIDŁOWE rozwiązanie zostanie wdrożone w systemie, który ponawia próbę połączenia za każdym razem - może to doprowadzić do uduszenia interfejsu sieciowego (przeciążenia) i ostatecznie spowodować zawieszenie się komputera! @Endless opublikował tutaj poprawną odpowiedź .
Slavik Meltser

2
@SlavikMeltser Nie rozumiem. Wskazana odpowiedź również nie przerywa połączenia TCP.
Mateus Pires

147

Bardzo podoba mi się czyste podejście od tego GIST wykorzystaniem 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
Powoduje to „Nieobsłużone odrzucenie”, jeśli po przekroczeniu limitu czasu wystąpi fetchbłąd . Można to rozwiązać poprzez obsługę ( ) błędu i ponowne zgłoszenie, jeśli limit czasu jeszcze nie nastąpił. .catchfetch
lionello

7
IMHO można to poprawić za pomocą AbortController podczas odrzucania, patrz stackoverflow.com/a/47250621 .
RiZKiT

Byłoby lepiej wyczyścić limit czasu, jeśli pobieranie również się powiedzie.
Bob9630

116

Użycie rozwiązania Promise Race spowoduje zawieszenie żądania i nadal zużywa przepustowość w tle oraz obniża maksymalne dozwolone współbieżne żądanie, które jest nadal w trakcie przetwarzania.

Zamiast tego użyj AbortController, aby faktycznie przerwać żądanie. Oto przykład

const controller = new AbortController()

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

fetch(url, { signal: controller.signal }).then(response => {
  // completed request before timeout fired

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

AbortController może być również używany do innych rzeczy, nie tylko do pobierania, ale także do odczytywalnych / zapisywalnych strumieni. Nowsze funkcje (szczególnie te oparte na obietnicach) będą z tego coraz częściej korzystać. NodeJS również zaimplementował AbortController w swoich strumieniach / systemie plików. Wiem, że przeglądają to również bluetooth sieciowy


16
Wygląda to nawet lepiej niż rozwiązanie wyścigu obietnic, ponieważ prawdopodobnie przerywa żądanie zamiast po prostu wziąć wcześniejszą odpowiedź. Popraw mnie, jeśli się mylę.
Karl Adler

3
Odpowiedź nie wyjaśnia, czym jest AbortController. Ponadto jest eksperymentalny i musi być wypełniany w nieobsługiwanych silnikach, również nie jest składnią.
Estus Flask

Może nie wyjaśnia, czym jest AbortController (dodałem link do odpowiedzi, aby ułatwić leniwym), ale jest to jak dotąd najlepsza odpowiedź, ponieważ podkreśla fakt, że samo ignorowanie żądania nie oznacza, że ​​nadal nie oczekujące. Świetna odpowiedź.
Aurelio

2
„Dodałem link do odpowiedzi, aby ułatwić pracę leniwym” - powinien zawierać link i więcej informacji zgodnie z zasadami. Ale dziękuję za poprawienie odpowiedzi.
Jay Wick,

8
Lepiej mieć tę odpowiedź niż brak odpowiedzi, ponieważ ludzie są odstraszani przez dziwactwo, tbh
Michael Terry,

26

Opierając się na doskonałej odpowiedzi Endless , stworzyłem przydatną funkcję użytkową.

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. Jeśli przekroczono limit czasu przed pobraniem zasobu, pobieranie jest przerywane.
  2. Jeśli zasób zostanie pobrany przed osiągnięciem limitu czasu, limit czasu zostanie wyczyszczony.
  3. Jeśli sygnał wejściowy zostanie przerwany, pobieranie zostanie przerwane, a limit czasu zostanie wyczyszczony.
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
        }
    });

Mam nadzieję, że to pomoże.


2
To jest fantastyczne! Obejmuje wszystkie nieprzyjemne przypadki, które były problematyczne w innych odpowiedziach, i podajesz jasny przykład użycia.
Atte Juvonen

8

nie ma jeszcze obsługi limitu czasu w interfejsie API pobierania. Ale można to osiągnąć, zawijając to w obietnicę.

np.

  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);
      }
    });
  }

Podoba mi się ten bardziej, mniej powtarzalny w użyciu więcej niż raz.
dandavis

1
Żądanie nie jest anulowane po przekroczeniu limitu czasu, prawda? Może to być w porządku dla OP, ale czasami chcesz anulować żądanie po stronie klienta.
trysis

2
@trysis dobrze, tak. Niedawno zaimplementowano rozwiązanie do przerywania pobierania z AbortController , ale wciąż eksperymentalne z ograniczoną obsługą przeglądarki. Dyskusja
code-jaff

To zabawne, tylko IE i Edge to obsługują! Chyba że mobilna witryna Mozilli znów
zacznie

Firefox obsługuje go od 57. :: oglądanie w Chrome ::
Franklin Yu

6

EDYTUJ : Żądanie pobierania będzie nadal działało w tle i najprawdopodobniej zarejestruje błąd w konsoli.

Rzeczywiście Promise.racepodejście jest lepsze.

Zobacz ten link, aby zapoznać się z Promise.race ()

Wyścig oznacza, że ​​wszystkie obietnice zostaną uruchomione w tym samym czasie, a wyścig zostanie zatrzymany, gdy tylko jedna z obietnic zwróci wartość. Dlatego zostanie zwrócona tylko jedna wartość . Możesz również przekazać funkcję do wywołania, jeśli upłynie limit czasu pobierania.

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

Jeśli to wzbudzi Twoje zainteresowanie, możliwa implementacja to:

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;
  });
}

1

Możesz utworzyć opakowanie timeoutPromise

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

Następnie możesz zawrzeć dowolną obietnicę

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

W rzeczywistości nie anuluje podstawowego połączenia, ale pozwoli ci przekroczyć limit czasu dla obietnicy.
Odniesienie


1

Jeśli nie skonfigurowałeś limitu czasu w swoim kodzie, będzie to domyślny limit czasu żądania Twojej przeglądarki.

1) Firefox - 90 sekund

Wpisz about:configadres URL przeglądarki Firefox. Znajdź wartość odpowiadającą kluczowinetwork.http.connection-timeout

2) Chrome - 300 sekund

Źródło



-1

Korzystając z c-promise2 lib, anulowane pobieranie z limitem czasu może wyglądać następująco ( Live jsfiddle demo ):

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

Ten kod jako pakiet npm cp-fetch

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.