.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 da Polly. Eles vêm de lugares diferentes, têm padrões diferentes e precisam de blocos separados catch .

Qual tempo limite foi disparado

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 manipulador padrão 10 segundos por tentativa Nada inicialmente: o manipulador faz 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 um HttpRequestException, em vez disso. 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, inclusive novas tentativas, demora mais de 30 segundos, o tempo limite total a cancela e o 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 padrões, consulte Configurações 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, capture 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 for cancelado, 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 deixe 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 determina se você encontra os bugs antes que seus usuários o façam.

Approach O que você encontra Do que você sente falta
Aguardar a produção Tempos limite reais Tudo, até que um usuário toque 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 lança 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 deixar a API lenta sob demanda
Interceptar o tráfego real do aplicativo e atrasar as respostas O HttpClient real, o manipulador de resiliência e as 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. O .NET usa o proxy do sistema, logo, você não precisa alterar o 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/v4.0.0/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/v4.0.0/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, depois de 30 segundos, o código receberá um TimeoutRejectedException. Em vez disso, para testar HttpClient.Timeout , defina minMs mais alto do que o tempo limite configurado e defina maxMs como o mesmo valor ou um valor mais alto. Se minMs for maior que maxMs, o Dev Proxy não será iniciado. 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