Buscar tempo limite de solicitação de API?

106

Eu tenho um fetch-api POSTpedido:

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

Quero saber qual é o tempo limite padrão para isso? e como podemos defini-lo para um valor específico como 3 segundos ou segundos indefinidos?

Akshay Lokur
fonte

Respostas:

78

Editar 1

Conforme apontado nos comentários, o código na resposta original continua executando o cronômetro mesmo depois que a promessa é resolvida / rejeitada.

O código abaixo corrige esse problema.

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


Resposta original

Não tem um padrão especificado; a especificação não discute os tempos limite.

Você pode implementar seu próprio wrapper de tempo limite para promessas em geral:

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

Conforme descrito em https://github.com/github/fetch/issues/175 Comentário por https://github.com/mislav

agitar
fonte
28
Por que essa é a resposta aceita? O setTimeout aqui continuará mesmo se a promessa for resolvida. Uma solução melhor seria fazer isso: github.com/github/fetch/issues/175#issuecomment-216791333
radtad
3
@radtad mislav defende sua abordagem mais abaixo nesse tópico: github.com/github/fetch/issues/175#issuecomment-284787564 . Não importa que o tempo limite continue, porque invocar .reject()uma promessa que já foi resolvida não adianta nada.
Mark Amery
1
embora a função 'fetch' seja rejeitada por tempo limite, a conexão tcp em segundo plano não é fechada. Como posso sair do meu processo de nó normalmente?
Prog Quester
28
PARE! Esta é uma resposta incorreta! Embora, pareça uma solução boa e funcional, mas na verdade a conexão não será fechada, o que eventualmente ocupará uma conexão TCP (pode ser até infinita - depende do servidor). Imagine esta solução ERRADA a ser implementada em um sistema que tenta novamente uma conexão a cada período de tempo - Isso pode levar à sufocação da interface de rede (sobrecarga) e fazer sua máquina travar eventualmente! @Endless postou a resposta correta aqui .
Slavik Meltser
2
@SlavikMeltser Não entendi. A resposta que você apontou também não interrompe a conexão TCP.
Mateus Pires
147

Eu realmente gosto da abordagem limpa desta essência usando 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
})
Karl Adler
fonte
2
Isso causa uma "rejeição não tratada" se fetchocorrer um erro após o tempo limite. Isso pode ser resolvido manipulando ( .catch) a fetchfalha e relançando se o tempo limite ainda não aconteceu.
lionello de
7
IMHO, isso pode ser melhorado com AbortController ao rejeitar, consulte stackoverflow.com/a/47250621 .
RiZKiT
Seria melhor limpar o tempo limite se a busca também for bem-sucedida.
Bob9630
116

Usar uma solução de corrida de promessa deixará a solicitação suspensa e ainda consumirá largura de banda em segundo plano e reduzirá o máximo de solicitações simultâneas permitidas enquanto ainda está em processo.

Em vez disso, use o AbortController para realmente abortar a solicitação. Aqui está um exemplo

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 também pode ser usado para outras coisas, não apenas para buscar, mas também para fluxos legíveis / graváveis. Funções mais novas (especialmente aquelas baseadas em promessa) usarão isso cada vez mais. NodeJS também implementou AbortController em seus streams / sistema de arquivos também. Eu sei que o bluetooth da web também está investigando isso

Sem fim
fonte
16
Isso parece ainda melhor do que a solução de corrida-promessa porque provavelmente aborta a solicitação em vez de apenas obter a resposta anterior. Corrija-me se eu estiver errado.
Karl Adler
3
A resposta não explica o que é AbortController. Além disso, é experimental e precisa ser polyfilled em motores não suportados, também não é uma sintaxe.
Estus Flask
Pode não explicar o que é AbortController (adicionei um link para a resposta para tornar mais fácil para os preguiçosos), mas esta é a melhor resposta até agora, pois destaca o fato de que simplesmente ignorar uma solicitação não significa que ela ainda não pendente. Ótima resposta.
Aurelio
2
“Eu adicionei um link para a resposta para tornar mais fácil para os preguiçosos” - deveria realmente vir com um link e mais informações conforme as regras tbh. Mas obrigado por melhorar a resposta.
Jay Wick de
8
Melhor ter essa resposta do que nenhuma resposta, porque as pessoas são desencorajadas por detalhes, tbh
Michael Terry
26

Com base na excelente resposta de Endless , criei uma função de utilidade útil.

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. Se o tempo limite for atingido antes que o recurso seja obtido, a busca será abortada.
  2. Se o recurso for buscado antes que o tempo limite seja atingido, o tempo limite será limpo.
  3. Se o sinal de entrada for abortado, a busca será abortada e o tempo limite apagado.
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
        }
    });

Espero que ajude.

Aadit M Shah
fonte
2
Isto é fantástico! Ele cobre todos os casos extremos desagradáveis ​​que foram problemáticos em outras respostas e você fornece um exemplo de uso claro.
Atte Juvonen
8

ainda não há suporte para tempo limite na API de busca. Mas isso poderia ser alcançado envolvendo-o em uma promessa.

por exemplo.

  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);
      }
    });
  }
código
fonte
eu gosto mais deste, menos repetitivo para usar mais de uma vez.
dandavis
1
A solicitação não é cancelada após o timeout aqui, correto? Isso pode ser bom para o OP, mas às vezes você deseja cancelar uma solicitação do lado do cliente.
trysis 01 de
2
@trysis bem, sim. Implementou recentemente uma solução para abort fetch com AbortController , mas ainda experimental com suporte de navegador limitado. Discussão
code-jaff
Isso é engraçado, o IE e o Edge são os únicos que o suportam! A menos que o site móvel da Mozilla esteja com problemas novamente ...
trysis
O Firefox oferece suporte desde 57. :: assistindo no Chrome ::
Franklin Yu
6

EDITAR : A solicitação de busca ainda estará em execução em segundo plano e provavelmente registrará um erro em seu console.

Na verdade, a Promise.raceabordagem é melhor.

Veja este link para referência Promise.race ()

Corrida significa que todas as promessas serão realizadas ao mesmo tempo e a corrida será interrompida assim que uma das promessas retornar um valor. Portanto, apenas um valor será retornado . Você também pode passar uma função para chamar se a busca expirar.

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

Se isso despertar seu interesse, uma possível implementação seria:

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;
  });
}
Arroganz
fonte
1

Você pode criar um wrapper timeoutPromise

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

Você pode então quebrar qualquer promessa

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

Na verdade, isso não cancelará uma conexão subjacente, mas permitirá que você expire uma promessa.
Referência

Pulkit Aggarwal
fonte
1

Se você não configurou o tempo limite em seu código, será o tempo limite de solicitação padrão do seu navegador.

1) Firefox - 90 segundos

Digite about:configno campo URL do Firefox. Encontre o valor correspondente à chavenetwork.http.connection-timeout

2) Chrome - 300 segundos

Fonte

Harikrishnan
fonte
0
  fetchTimeout (url,options,timeout=3000) {
    return new Promise( (resolve, reject) => {
      fetch(url, options)
      .then(resolve,reject)
      setTimeout(reject,timeout);
    })
  }
Mojimi
fonte
É praticamente o mesmo que stackoverflow.com/a/46946588/1008999, mas você tem um tempo limite padrão
Endless
-1

Usando c-promessa2 lib, a busca cancelável com tempo limite pode ser semelhante a esta ( demonstração ao vivo do 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

Este código como um pacote npm cp-fetch

Dmitriy Mozgovoy
fonte