.NET HttpClient timeouts: TaskCanceledException e TimeoutRejectedException

Quando uma API é demasiado lenta, a tua aplicação .NET recebe uma de duas exceções, dependendo do timeout que foi disparado. HttpClient.Timeout lança um TaskCanceledException. O processador padrão de resiliência proveniente de Microsoft.Extensions.Http.Resilience lança um TimeoutRejectedException da Polly. Vêm de sítios diferentes, têm predefinições diferentes e precisam de blocos separados catch.

Qual foi o timeout que ocorreu

Timeout Default O que o teu código recebe Onde o colocaste
HttpClient.Timeout 100 segundos TaskCanceledException, com a TimeoutException como seu InnerException (.NET 5 e posteriores) HttpClient.Timeout
Timeout da tentativa do manipulador padrão 10 segundos por tentativa Nada no início: o processador repete a tentativa AddStandardResilienceHandler(options => ...)
Tempo limite total do processador predefinido 30 segundos, incluindo todas as novas tentativas Polly.Timeout.TimeoutRejectedException AddStandardResilienceHandler(options => ...)

HttpClient.Timeout

HttpClient.Timeout aplica-se a todos os pedidos que a HttpClient instância envia. Para usar um timeout diferente para um pedido, passe a CancellationToken de um CancellationTokenSource com o seu próprio timeout. O mais curto dos dois aplica-se. Defina Timeout.InfiniteTimeSpan para o desligar.

No .NET 5 e seguintes, um timeout lança um TaskCanceledException com um TimeoutException no interior. Nas versões anteriores do .NET Core, a exceção interna não existe. No .NET Framework, obtém-se um HttpRequestException em vez disso. Para detalhes, consulte HttpClient.Timeout e faça pedidos HTTP com a classe HttpClient.

Um erro TaskCanceledException também significa que alguém cancelou o pedido, por exemplo, um utilizador que fechou a página. Para distinguir um timeout de um cancelamento, verifique ex.InnerException is TimeoutException ou verifique se o seu próprio token está cancelado.

O manipulador de resiliência padrão

AddStandardResilienceHandler() Encadeia um limitador de taxa, um timeout total, uma retentativa, um disjuntor de circuito e um timeout de tentativa. Quando uma tentativa demora mais de 10 segundos, o tempo limite da tentativa cancela-a e a estratégia de repetição tenta novamente: até 3 repetições, com recuo exponencial e variação aleatória, começando aos 2 segundos. Quando todo o pedido, incluindo novas tentativas, demora mais de 30 segundos, o timeout total cancela-o e o seu código recebe um erro TimeoutRejectedException.

TimeoutRejectedException deriva de Exception. Não é nem a TimeoutException nem um HttpRequestException, por isso um bloco catch (HttpRequestException) não o apanha. Para a lista completa de predefinições, veja predefinições do processador de resiliência padrão.

Por exemplo, quando uma aplicação .NET 10 que usa os valores predefinidos do handler padrão chama uma API que demora entre 11 a 15 segundos por resposta, a aplicação recebe um TimeoutRejectedException após 30 segundos, e o seu bloco catch (HttpRequestException) não é executado.

Como gerir os timeouts do HttpClient

  1. Capture ambas as exceções na chamada à API. Se usares o handler padrão, captura TimeoutRejectedException. Obtém TaskCanceledException para HttpClient.Timeout.
  2. Distinga um tempo limite de um cancelamento. Só trate a TaskCanceledException como um timeout quando a sua exceção interna for um TimeoutException. Quando o interlocutor cancelou, pare silenciosamente.
  3. Escolha tempos de espera adequados à API. Se a API demorar frequentemente mais de 10 segundos, altere os tempos limite da tentativa e o tempo limite total em AddStandardResilienceHandler(options => ...).
  4. Não volte a tentar POST ou PATCH a não ser que a API o torne seguro. O handler padrão repete as tentativas de todos os métodos por defeito, incluindo POST. Chamar options.Retry.DisableForUnsafeHttpMethods() para excluir POST, PATCH, PUT, DELETE e CONNECT, ou options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch) para continuar a tentar novamente PUT e DELETE idempotentes.
  5. Diz ao utilizador o que aconteceu. Mostra "o serviço está lento, tenta novamente" em vez de um erro genérico.
using Polly.Timeout;

public async Task<string?> GetForecastAsync(HttpClient client, CancellationToken cancellationToken)
{
    try
    {
        return await client.GetStringAsync("https://api.contoso.com/forecast", cancellationToken);
    }
    catch (TimeoutRejectedException)
    {
        // Standard resilience handler: total timeout expired after all retries
        return null;
    }
    catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
    {
        // HttpClient.Timeout expired
        return null;
    }
    catch (HttpRequestException)
    {
        // Network error, or an error status code after all retries
        return null;
    }
}

Como testar se a sua aplicação lida com tempos limite

Raramente vês um timeout enquanto desenvolves, por isso os teus blocos catch raramente são executados. A forma como testas decide se encontras os bugs antes dos teus utilizadores.

Approach O que encontra Do que sentes falta
Aguardar o ambiente de produção Timeouts reais Tudo, até que um utilizador lhe toque
Simula a API nos teus testes, ou deixa o teu agente de programação escrever o mock Se o teu catch bloco é executado, se o mock lança a exceção certa A tua verdadeira HttpClient configuração, as tentativas do handler de resiliência, e a exceção que ele realmente lança. A tua aplicação também precisa de um switch só de teste para chegar ao mock.
Chame a API real e espere que seja lenta Comportamento real Não podes tornar a API lenta quando quiseres
Intercete o tráfego real da sua aplicação e atrase as respostas O seu verdadeiro HttpClient, manipulador de resiliência e exceções Nada na tua aplicação muda, por isso isso não testa o teu código isoladamente. Deixa isso para os testes unitários.

Experimente-o na sua aplicação

Dev Proxy interceta os pedidos da sua aplicação para a API e atrasa as respostas com o LatencyPlugin. O .NET usa o proxy do sistema, por isso não precisas de alterar o teu código. Este exemplo atrasa cada resposta em 11 a 15 segundos, mais tempo do que o tempo de espera de tentativa de 10 segundos do manipulador padrão. Substitui https://api.contoso.com pelo URL da API que a tua app chama.

Ficheiro: devproxyrc.json

{
  "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/rc.schema.json",
  "plugins": [
    {
      "name": "LatencyPlugin",
      "enabled": true,
      "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
      "configSection": "slowApi"
    }
  ],
  "urlsToWatch": [
    "https://api.contoso.com/*"
  ],
  "slowApi": {
    "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/latencyplugin.schema.json",
    "minMs": 11000,
    "maxMs": 15000
  }
}

Inicia o Dev Proxy com devproxy --config-file devproxyrc.json e executa a tua aplicação. Cada tentativa atinge o tempo limite, o handler tenta novamente e, após 30 segundos, o seu código recebe um TimeoutRejectedException. Para testar HttpClient.Timeout , defina minMs um valor superior ao timeout que configurou. Para detalhes de configuração, veja Usar Proxy de Desenvolvimento com aplicações .NET. Para instalar Dev Proxy, consulte Configurar Dev Proxy.

Passos seguintes

Ver também