.NET timeouts do HttpClient: TaskCanceledException e TimeoutRejectedException

Quando uma API é muito lenta, seu aplicativo .NET obtém 1 de 2 exceções, dependendo de qual timeout ocorreu. HttpClient.Timeout lança um TaskCanceledException. O manipulador de resiliência padrão de Microsoft.Extensions.Http.Resilience lança um TimeoutRejectedException do Polly. Eles vêm de lugares diferentes, têm padrões diferentes e precisam de blocos separados catch .

Qual timeout ocorreu

Intervalo Padrão O que seu código recebe Onde você configura isso
HttpClient.Timeout 100 segundos TaskCanceledException, com um TimeoutException como seu InnerException (.NET 5 e posteriores) HttpClient.Timeout
Tempo limite da tentativa do processador padrão 10 segundos por tentativa Nada a princípio: o handler tenta novamente a tentativa AddStandardResilienceHandler(options => ...)
Tempo limite total do manipulador padrão 30 segundos, incluindo todas as novas tentativas Polly.Timeout.TimeoutRejectedException AddStandardResilienceHandler(options => ...)

HttpClient.Timeout

HttpClient.Timeout aplica-se a cada solicitação que a instância HttpClient envia. Para usar um tempo limite diferente para uma solicitação, passe um CancellationToken a partir de um CancellationTokenSource com seu próprio tempo limite. O mais curto dos dois se aplica. Defina Timeout.InfiniteTimeSpan para desativá-lo.

No .NET 5 ou superior, um timeout lança um TaskCanceledException com uma TimeoutException interna. Em versões anteriores do .NET Core, a exceção interna não está lá. No .NET Framework, você obtém uma instância de HttpRequestException. Para obter detalhes, consulte HttpClient.Timeout e faça solicitações HTTP com a classe HttpClient.

Um TaskCanceledException também significa que alguém cancelou a solicitação, por exemplo, um usuário que fechou a página. Para distinguir um tempo limite de um cancelamento, verifique ex.InnerException is TimeoutException, ou verifique se seu próprio token foi cancelado.

O manipulador de resiliência padrão

AddStandardResilienceHandler() encadeia um limitador de taxa, um tempo limite total, uma nova tentativa, um circuit breaker e um tempo limite de tentativa. Quando uma tentativa leva mais de 10 segundos, o tempo limite da tentativa a cancela e a estratégia de repetição tenta novamente: até três novas tentativas, com backoff exponencial e jitter, começando em 2 segundos. Quando toda a solicitação, incluindo novas tentativas, leva mais de 30 segundos, o tempo limite total a cancela e seu código recebe um TimeoutRejectedException.

TimeoutRejectedException deriva de Exception. Não é nem um TimeoutException nem um HttpRequestException, então um bloco catch (HttpRequestException) não o captura. Para obter a lista completa de configurações padrão, consulte configurações padrão do manipulador de resiliência padrão.

Por exemplo, quando um aplicativo .NET 10 que usa os padrões do manipulador padrão chama uma API que leva de 11 a 15 segundos por resposta, o aplicativo obtém um TimeoutRejectedException após 30 segundos e seu catch (HttpRequestException) bloco não é executado.

Como lidar com tempos limite do HttpClient

  1. Trate ambas as exceções no ponto em que você chama a API. Se você usar o manipulador padrão, pegue TimeoutRejectedException. Capture TaskCanceledException para HttpClient.Timeout.
  2. Diferencie um timeout de um cancelamento. Só trate um TaskCanceledException como tempo limite quando sua exceção interna for um TimeoutException. Quando o chamador cancelar, pare em silêncio.
  3. Escolha timeouts adequados à API. Se a API geralmente demorar mais de 10 segundos, altere os tempos limite de tentativa e total em AddStandardResilienceHandler(options => ...).
  4. Não tente novamente POST ou PATCH a menos que a API torne isso seguro. O manipulador padrão tenta novamente todos os métodos por padrão, incluindo POST. Chame options.Retry.DisableForUnsafeHttpMethods() para excluir POST, PATCH, PUT, DELETE e CONNECT, ou options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch) para continuar tentando novamente PUT e DELETE idempotentes.
  5. Diga ao usuário o que aconteceu. Mostrar "o serviço está lento, tente 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 seu aplicativo trata timeouts

Você raramente se depara com um timeout enquanto desenvolve, então seus blocos catch raramente são executados. A maneira como você testa decide se encontra os bugs antes que seus usuários os encontrem.

Approach O que você encontra Do que você sente falta
Aguardar a produção Timeouts reais Tudo, até que um usuário clique nele
Simular a API em seus testes ou deixar seu agente de codificação escrever a simulação Se o bloco catch é executado, se a simulação gera a exceção certa Sua configuração real HttpClient , as novas tentativas do manipulador de resiliência e a exceção que ele realmente gera. Seu aplicativo também precisa de uma opção somente de teste para acessar o mock.
Chame a API real e espere que seja lenta Comportamento real Você não pode tornar a API lenta quando quiser
Interceptar o tráfego real do aplicativo e atrasar as respostas Seu verdadeiro HttpClient, gerenciador de resiliência e exceções Nada no aplicativo muda, portanto, ele não testa seu código de forma isolada. Mantenha seus testes de unidade para isso.

Experimente em seu aplicativo

Dev Proxy intercepta as solicitações do aplicativo para a API e atrasa as respostas com o LatencyPlugin. .NET usa o proxy do sistema, portanto, você não precisa alterar seu código. Este exemplo adiciona um atraso de 11 a 15 segundos a cada resposta, mais do que o tempo limite de tentativa de 10 segundos do manipulador padrão. Substitua https://api.contoso.com pela URL da API que seu aplicativo usa.

Arquivo: 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
  }
}

Inicie o Dev Proxy com devproxy --config-file devproxyrc.json e execute o aplicativo. Se cada tentativa expirar, o manipulador tentará novamente e, após 30 segundos, seu código receberá TimeoutRejectedException. Para testar HttpClient.Timeout em vez disso, defina minMs para um valor maior do que o tempo limite que você configurou. Para obter detalhes de configuração, consulte Usando o Dev Proxy com aplicativos .NET. Para instalar o Dev Proxy, consulte Configurar o Dev Proxy.

Próximas Etapas 

Consulte também