Runtime: HttpClient lança TaskCanceledException no tempo limite

Criado em 25 mai. 2017  ·  119Comentários  ·  Fonte: dotnet/runtime

HttpClient está lançando um TaskCanceledException no tempo limite em algumas circunstâncias. Isso está acontecendo para nós quando o servidor está sob carga pesada. Conseguimos contornar aumentando o tempo limite padrão. O seguinte tópico do fórum do MSDN captura a essência do problema experimentado: https://social.msdn.microsoft.com/Forums/en-US/d8d87789-0ac9-4294-84a0-91c9fa27e353/bug-in-httpclientgetasync-should-throw -webexception-not-taskcanceledexception?forum=netfxnetcom&prof=required

Obrigado

area-System.Net.Http enhancement

Comentários muito úteis

@karelz adicionando uma nota como outro cliente afetado por essa exceção enganosa :)

Todos 119 comentários

@awithy qual versão do .NET Core você está usando?

1.1

Seja o design correto ou não, por design OperationCanceledExceptions são lançadas para tempos limite (e TaskCanceledException é uma OperationCanceledException).
cc: @davidsh

Nossa equipe achou isso pouco intuitivo, mas parece estar funcionando conforme o planejado. Infelizmente, quando chegamos a isso, perdemos um pouco de tempo tentando encontrar uma fonte de cancelamento. Ter que lidar com esse cenário de forma diferente do cancelamento de tarefas é muito feio (criamos um manipulador personalizado para gerenciar isso). Isso também parece ser um uso excessivo de cancelamento como um conceito.

Obrigado novamente por ti resposta rápida.

Parece que por design + houve apenas 2 reclamações de clientes nos últimos 6 meses.
Fechando.

@karelz adicionando uma nota como outro cliente afetado por essa exceção enganosa :)

Obrigado pela informação, por favor, vote também no post principal (os problemas podem ser classificados por ele), obrigado!

Seja o design correto ou não, por design OperationCanceledExceptions são lançadas para tempos limite

Este é um design ruim IMO. Não há como saber se a solicitação foi realmente cancelada (ou seja, o token de cancelamento passado para SendAsync foi cancelado) ou se o tempo limite foi decorrido. No último caso, faria sentido tentar novamente, enquanto no primeiro caso não faria. Existe um TimeoutException que seria perfeito para este caso, por que não usá-lo?

Sim, isso também jogou nossa equipe para um loop. Há um thread inteiro do Stack Overflow dedicado a tentar lidar com isso: https://stackoverflow.com/questions/10547895/how-can-i-tell-when-httpclient-has-timed-out/32230327#32230327

Publiquei uma solução alternativa há alguns dias: https://www.thomaslevesque.com/2018/02/25/better-timeout-handling-with-httpclient/

Maravilha, obrigado.

Reabrindo, pois o interesse agora é maior - o thread do StackOverflow agora tem 69 votos positivos.

cc @dotnet/ncl

Alguma novidade ? Ainda acontece no dotnet core 2.0

Está na nossa lista dos principais problemas para o pós-2.1. Não será corrigido em 2.0/2.1 embora.

Temos várias opções do que fazer quando o tempo limite acontece:

  1. Jogue TimeoutException em vez de TaskCanceledException .

    • Pro: Fácil de distinguir o tempo limite da ação de cancelamento explícita em tempo de execução

    • Contras: Mudança de quebra técnica - novo tipo de exceção é lançado.

  2. Jogue TaskCanceledException com exceção interna como TimeoutException

    • Pro: Possível distinguir o tempo limite da ação de cancelamento explícito em tempo de execução. Tipo de exceção compatível.

    • Pergunta aberta: É correto jogar fora a pilha TaskCanceledException original e jogar uma nova, preservando InnerException (como TimeoutException.InnerException )? Ou devemos preservar e apenas embrulhar o TaskCanceledException original?



      • Ou: novo TaskCanceledException -> novo TimeoutException -> TaskCanceledException original (com InnerException original que pode ser nula)


      • Ou: new TaskCanceledException -> new TimeoutException -> original TaskCanceledException.InnerException (pode ser nulo)



  3. Jogue TaskCanceledException com uma mensagem mencionando o tempo limite como o motivo

    • Pro: Possível distinguir o tempo limite da ação de cancelamento explícito da mensagem/logs

    • Contras: Não pode ser distinguido em tempo de execução, é "somente depuração".

    • Pergunta aberta: O mesmo que em [2] - devemos envolver ou substituir o TaskCanceledException original (e empilhar)

Estou inclinado para a opção [2], com o descarte da pilha original de TaskCanceledException .
@stephentoub @davidsh alguma opinião?

BTW: A mudança deve ser bastante direta em HttpClient.SendAsync onde configuramos o tempo limite:
https://github.com/dotnet/corefx/blob/30fb78875148665b98748ede3013641734d9bf5c/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L433 -L461

A exceção de nível superior deve sempre ser uma HttpRequestException. Esse é o modelo de API padrão para HttpClient.

Dentro disso, como exceção interna, "timeouts" provavelmente deve ser um TimeoutException. Na verdade, se combinarmos isso com os outros problemas (sobre como modificar HttpRequestion para incluir uma nova propriedade enum semelhante a WebExceptionStatus), os desenvolvedores precisariam apenas verificar essa enumeração e não precisariam inspecionar exceções internas.

Esta é basicamente a opção 1), mas continuando a preservar o modelo de API de compatibilidade de aplicativo atual que as exceções de nível superior das APIs HttpClient são sempre HttpRequestException.

Observação: qualquer "tempo limite" da leitura direta de um fluxo de resposta HTTP de APIs de fluxo como:

``` c#
var cliente = new HttpClient();
Stream responseStream = aguarde cliente.GetStreamAsync(url);

int bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length);
```

deve continuar a lançar System.IO.IOException etc. como exceções de nível superior (na verdade, temos alguns bugs no SocketsHttpHandler sobre isso).

@davidsh você está sugerindo lançar HttpRequestException mesmo nos casos em que há cancelamento explícito do usuário? Aqueles jogam TaskCanceledException hoje e parece bom. Não tenho certeza se é algo que queremos mudar...
Eu ficaria bem em expor os tempos limite do lado do cliente como HttpRequestException em vez de TaskCanceledException .

@davidsh você está sugerindo lançar HttpRequestException mesmo nos casos em que há cancelamento explícito do usuário? Aqueles lançam TaskCanceledException hoje e parece bom. Não tenho certeza se é algo que queremos mudar…

Preciso testar os diferentes comportamentos das pilhas, incluindo o .NET Framework, para verificar os comportamentos atuais exatos antes de responder a essa pergunta.

Além disso, em alguns casos, algumas de nossas pilhas estão lançando HttpRequestException com exceção interna de OperationCanceledException. TaskCanceledException é um tipo derivado de OperationCanceledException.

quando há cancelamento explícito do usuário

Também precisamos de uma definição clara do que é "explícito". Por exemplo, está definindo a propriedade HttpClient.Timeout "explícita"? Acho que estamos dizendo não. Que tal chamar .Cancel() no objeto CancellationTokenSource (que afeta o objeto passado no CancellationToken)? Isso é "explícito"? Provavelmente existem outros exemplos em que podemos pensar.

O .NET Framework também lança TaskCanceledException quando você define HttpClient.Timeout .

Se você não conseguir distinguir entre o cancelamento do cliente e o cancelamento da implementação - apenas marque ambos. Antes de usar o token do cliente, crie um derivado:
``` c#
var clientToken = ....
var innerTokenSource = CancellationTokenSource.CreateLinkedTokenSource(clientToken);

If at some point you catch **OperationCancelledException**:
```c#
try
{
     //invocation with innerToken
}
catch(OperationCancelledException oce)
{
      if(clientToken.IsCancellationRequested)
          throw; //as is, it is client initiated and in higher priority
      if(innerToken.IsCancellationRequested)
           //wrap it in HttpException, TimeoutException, whatever, wrap it in another linked token until you process all cases.
}

Basicamente, você filtra todas as camadas possíveis do token do cliente para o mais profundo, até encontrar o token que causou o cancelamento.

Alguma atualização sobre isso fazendo 2.2 ou 3.0? @karelz @davidsh , cc @NickCraver . Eu meio que gostaria que apenas fizesse a opção 1) de lançar um TimeoutException ou, alternativamente, um novo HttpTimeoutException derivado de HttpRequestException. Obrigado!

Alguma atualização sobre isso fazendo 2.2 ou 3.0?

Tarde demais para 2.2, foi lançado semana passada ;)

Tarde demais para 2.2, foi lançado semana passada ;)

Argh, é isso que acontece quando eu faço uma pausa ;)

Embora um pouco pateta, a solução alternativa mais simplista que encontrei é aninhar um manipulador de captura para OperationCanceledException e, em seguida, avaliar o valor de IsCancellationRequested para determinar se o token de cancelamento foi ou não o resultado de um tempo limite ou um aborto de sessão real. Claramente, a lógica que executa a chamada da API provavelmente estaria em uma lógica de negócios ou camada de serviço, mas consolidei o exemplo para simplificar:

````csharp
[HttpGet]
Tarefa assíncrona públicaIndex(Token de cancelamentoToken de cancelamento)
{
experimentar
{
//Isso normalmente seria feito em uma lógica de negócios ou camada de serviço, e não no próprio controlador, mas estou ilustrando o ponto aqui para simplificar
experimentar
{
//Usando HttpClientFactory para instâncias HttpClient
var httpCliente = _httpClientFactory.CreateClient();

        //Set an absurd timeout to illustrate the point
        httpClient.Timeout = new TimeSpan(0, 0, 0, 0, 1);

        //Perform call that requires special timeout logic                
        var httpResponse = await httpClient.GetAsync("https://someurl.com/api/long/running");

        //... (if GetAsync doesn't fail, handle the response as desired)
    }
    catch (OperationCanceledException innerOperationCanceled)
    {
        //If a canceled token exception occurs due to a timeout, "IsCancellationRequested" should be false
        if (cancellationToken.IsCancellationRequested)
        {
            //Bubble exception to global handler
            throw innerOperationCanceled;
        }
        else
        {
            //... (perform timeout logic here)
        }                    
    }                

}
catch (OperationCanceledException operationCanceledEx)
{
    _logger.LogWarning(operationCanceledEx, "Request was aborted by the end user.");
    return new StatusCodeResult(499);
}
catch (Exception ex)
{
    _logger.LogError(ex, "An unexepcted error occured.");
    return new StatusCodeResult(500);
}

}
````

Eu vi outros artigos sobre como lidar com esse cenário de forma mais elegante, mas todos eles exigem cavar no pipeline etc. Percebo que essa abordagem não é perfeita, espero que haja um suporte melhor para exceções de tempo limite reais com um futuro lançamento.

Trabalhar com o cliente http é totalmente abismal. HttpClient precisa ser excluído e reiniciado do zero.

Embora um pouco pateta, a solução alternativa mais simplista que encontrei é aninhar um manipulador de captura para OperationCanceledException e, em seguida, avaliar o valor de IsCancellationRequested para determinar se o token de cancelamento foi ou não o resultado de um tempo limite ou um aborto de sessão real. Claramente, a lógica que executa a chamada da API provavelmente estaria em uma lógica de negócios ou camada de serviço, mas consolidei o exemplo para simplificar:

[HttpGet]
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
  try
  {
      //This would normally be done in a business logic or service layer rather than in the controller itself but I'm illustrating the point here for simplicity sake
      try
      {
          //Using HttpClientFactory for HttpClient instances      
          var httpClient = _httpClientFactory.CreateClient();

          //Set an absurd timeout to illustrate the point
          httpClient.Timeout = new TimeSpan(0, 0, 0, 0, 1);

          //Perform call that requires special timeout logic                
          var httpResponse = await httpClient.GetAsync("https://someurl.com/api/long/running");

          //... (if GetAsync doesn't fail, handle the response as desired)
      }
      catch (OperationCanceledException innerOperationCanceled)
      {
          //If a canceled token exception occurs due to a timeout, "IsCancellationRequested" should be false
          if (cancellationToken.IsCancellationRequested)
          {
              //Bubble exception to global handler
              throw innerOperationCanceled;
          }
          else
          {
              //... (perform timeout logic here)
          }                    
      }                

  }
  catch (OperationCanceledException operationCanceledEx)
  {
      _logger.LogWarning(operationCanceledEx, "Request was aborted by the end user.");
      return new StatusCodeResult(499);
  }
  catch (Exception ex)
  {
      _logger.LogError(ex, "An unexepcted error occured.");
      return new StatusCodeResult(500);
  }
}

Eu vi outros artigos sobre como lidar com esse cenário de forma mais elegante, mas todos eles exigem cavar no pipeline etc. Percebo que essa abordagem não é perfeita, espero que haja um suporte melhor para exceções de tempo limite reais com um futuro lançamento.

É absolutamente maluco que precisamos escrever esse código atroz. HttpClient é a abstração mais vazada já criada pela Microsoft

Concordo que parece um pouco míope não incluir informações de exceção mais específicas em torno dos tempos limite. Parece uma coisa bastante básica para as pessoas quererem lidar com os tempos limites reais de maneira diferente das exceções de token canceladas.

Trabalhar com o cliente http é totalmente abismal. HttpClient precisa ser excluído e reiniciado do zero.\

Isso não é um feedback muito útil ou construtivo @dotnetchris . Clientes e funcionários da MSFT estão todos aqui para tentar melhorar as coisas, e é por isso que pessoas como @karelz ainda estão aqui ouvindo o feedback dos clientes e tentando melhorar. Não há nenhuma exigência para eles fazerem desenvolvimento a céu aberto, e não vamos queimar sua boa vontade com negatividade ou eles podem decidir pegar a bola e ir para casa, e isso seria pior para a comunidade. Obrigado! :)

Por que não basta criar um HttpClient2 (nome temporário) que tenha o comportamento esperado e informar, na documentação, que "HttpClient está obsoleto, por favor use o HttpClient2" e expliquei a diferença entre os 2 deles.

Dessa forma, nenhuma alteração importante é inserida no .NetFramework.

HttpClient não deve ser baseado em lançar exceções. Não é excepcional que um host de internet esteja indisponível. Na verdade, é esperado, dadas as infinitas permutações de endereços, o caso excepcional é fornecer algo que funcione.

Eu até escrevi sobre o quão ruim é o uso do HttpClient há mais de 3 anos. https://dotnetchris.wordpress.com/2015/12/11/working-with-httpclient-is-absurd/

Este é um design gritantemente ruim.

Alguém na microsoft ainda usou HttpClient? Com certeza não parece.

Então, além dessa superfície de API horrível, há todos os tipos de abstrações com vazamentos sobre a construção e o encadeamento. Eu não posso acreditar que é possível ser aproveitável e apenas novo é uma solução viável. HttpClient2, HttpClientSlim está bom ou qualquer outra coisa.

O acesso HTTP nunca deve lançar exceções. Deve sempre retornar um resultado, o resultado deve poder ser inspecionado para conclusão, tempo limite, resposta http. A única razão pela qual deve ser permitido lançar é se você invocar EnsureSuccess() , então não há problema em lançar qualquer exceção no mundo. Mas deve haver manipulação de fluxo de controle zero para um comportamento totalmente normal da Internet: host ativo, host não ativo, tempo limite do host. Nenhum deles é excepcional.

HttpClient não deve ser baseado em lançar exceções. Não é excepcional que um host de internet esteja indisponível. Na verdade, é esperado, dadas as infinitas permutações de endereços, o caso excepcional é fornecer algo que funcione.

@dotnetchris , já que parece que você tem muita paixão por esse tópico, por que não dedicar um tempo para escrever sua API completa proposta para HttpClient2 e enviá-la à equipe para discussão como um novo problema?

Eu não me preocupei em mergulhar nos detalhes do que torna o HttpClient bom ou ruim de um nível técnico mais profundo, então não tenho nada a acrescentar aos comentários de @dotnetchris. Eu definitivamente não acho apropriado criticar a equipe de desenvolvimento do MS, mas também entendo o quão frustrante é passar horas e horas pesquisando soluções para situações que aparentemente deveriam ser super simples de lidar. É a frustração de lutar para entender por que certas decisões/mudanças foram feitas, esperando que alguém da MS aborde a preocupação enquanto ainda tenta fazer seu trabalho e entregar nesse meio tempo.

No meu caso, tenho um serviço externo que chamo durante o processo OAuth para estabelecer funções de usuário e, se essa chamada expirar dentro de um período de tempo especificado (o que infelizmente acontece mais do que eu gostaria), preciso reverter para um método secundário .

Realmente, há uma infinidade de outras razões pelas quais posso ver por que alguém gostaria de distinguir entre um tempo limite e um cancelamento. Por que um melhor suporte para um cenário comum como esse parece não existir é o que eu luto para entender e realmente espero que seja algo que a equipe do MS pense mais.

@karelz o que seria necessário para obter isso dentro do cronograma para 3.0, uma proposta de API de alguém?

Não se trata de proposta de API. Trata-se de encontrar a implementação certa. Está em nossa lista de pendências 3.0 e bem no topo da lista (atrás de HTTP/2 e cenários corporativos). É muito boa chance de ser abordado em 3.0 por nossa equipe.
Claro que se alguém da comunidade estiver motivado, nós também daremos uma contribuição.

@karelz como a comunidade pode ajudar nesse caso? A razão pela qual eu mencionei se uma proposta de API era necessária é que eu não tinha certeza da sua mensagem que começa com "Temos várias opções do que fazer quando o tempo limite acontece:" se a equipe decidiu sua alternativa preferida das que você listou. Se você decidiu em uma direção, então podemos ir de lá. Obrigado!

Tecnicamente falando, não é uma questão de proposta de API. A decisão não exigirá a aprovação da API. No entanto, concordo que devemos estar alinhados sobre como expor a diferença - provavelmente haverá alguma discussão.
Não espero que as opções sejam muito diferentes umas das outras na implementação -- se a comunidade quiser ajudar, obter a implementação de qualquer uma das opções nos levará a 98% (assumindo que a exceção é lançada de um lugar e pode ser facilmente alterado posteriormente para se adaptar às opções [1]-[3] listadas acima).

Oi! Também abordamos esse problema recentemente. Apenas curioso se ainda está no seu radar para 3.0?

Sim, ele ainda está na nossa lista 3.0 - eu ainda daria 90% de chance de ser resolvido no 3.0.

Passei por isso hoje. Eu defino um tempo limite no HttpClient e isso me dá isso

{System.Threading.Tasks.TaskCanceledException: A task was canceled.}
CancellationRequested = true

Concordo com os outros, isso é confuso, pois não menciona o tempo limite como o problema.

Umm, ok, estou um pouco confuso. Todos neste segmento parecem estar dizendo que os tempos limite HTTP lançam um TaskCanceledException mas não é isso que estou recebendo... Estou recebendo um OperationCanceledException no .NET Core 2.1...

Se você quiser lançar um OperationCanceledException para tempos limite, tudo bem, posso contornar isso, mas passei horas tentando rastrear o que diabos estava acontecendo porque não está documentado corretamente:

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync

Ele afirma claramente que um HttpRequestException é lançado para tempos limite ...

TaskCanceledException deriva de OperationCanceledException.

@stephentoub Sim, mas não estou recebendo TaskCanceledException , estou recebendo OperationCanceledException .

Como em um bloco catch que captura TaskCanceledException não captura a exceção, ele precisa especificamente de um bloco catch para um OperationCanceledException .

Agora estou testando especificamente os tempos limite de HTTP filtrando o tipo de exceção, e a maneira como sei que é um tempo limite é se NÃO for um TaskCanceledException :

``` c#
experimentar {
usando (var response = await s_httpClient.SendAsync(request, _updateCancellationSource.Token).ConfigureAwait(false))
return (int)resposta.StatusCode < 400;
}
catch (OperationCanceledException ex) when (ex.GetType() != typeof(TaskCanceledException)) {
// Tempo limite HTTP
retorna falso;
}
catch (HttpRequestException) {
retorna falso;
}

or alternatively this works as well, which might be better:

```c#
            try {
                using (var response = await s_httpClient.SendAsync(request, _updateCancellationSource.Token).ConfigureAwait(false))
                    return (int)response.StatusCode < 400;
            }
            catch (OperationCanceledException ex) when (ex.GetType() == typeof(OperationCanceledException)) {
                // HTTP timeout
                return false;
            }
            catch (HttpRequestException) {
                return false;
            }

Umm, ok, estou um pouco confuso. Todos neste segmento parecem estar dizendo que os tempos limite HTTP lançam um TaskCanceledException mas não é isso que estou recebendo... Estou recebendo um OperationCanceledException no .NET Core 2.1...

Se você quiser lançar um OperationCanceledException para tempos limite, tudo bem, posso contornar isso, mas passei horas tentando rastrear o que diabos estava acontecendo porque não está documentado corretamente:

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.sendasync

Ele afirma claramente que um HttpRequestException é lançado para tempos limite ...

Você leu todo o thread porque, na verdade, há várias respostas em que OperationCanceledExecption é referenciado, incluindo o exemplo de código que apresentei que mostra uma maneira (não tão limpa quanto eu gostaria, mas é uma maneira) de lidar explicitamente com os tempos limite.

@Hobray Sim, eu tenho - o que faz você pensar que não? Isso ainda não explica quantas pessoas neste tópico estão supostamente recebendo TaskCanceledException em tempos limite, mas eu não estou.

Eu vi seu exemplo de código também, mas infelizmente ele tem uma condição de corrida que eu não acredito que você tenha considerado. O código postado que estou usando evita esse problema e não requer acesso ao token de cancelamento, o que pode ser importante se você estiver tentando capturar o tempo limite mais a montante.

@mikernet Talvez o texto tenha me confundido. Você está certo que eu não tinha considerado uma possível condição de corrida. Eu sei que não é uma solução incrível, mas se você quiser explicar o que eu esqueci, eu gostaria de ouvi-lo.

Uma coisa que notei é que a exceção exata que você obtém parece variar dependendo se o tempo limite ou o cancelamento ocorre ou não em uma invocação de controlador vs middleware. Dentro de uma invocação de middleware parece ser apenas TaskCanceledException . Infelizmente, não tenho tempo para vasculhar as bibliotecas de código do .NET Core para entender o porquê, mas é algo que notei.

@Hobray Haha sim, claro - desculpe, eu não quis ser enigmático sobre a condição da corrida, eu estava apenas no meu telefone enquanto estava em movimento digitando a última mensagem que dificultava a explicação.

A condição de corrida em sua solução ocorre se um cancelamento de tarefa for iniciado entre o tempo limite HttpClient e o ponto em que você verifica o estado de cancelamento do token de cancelamento. Na minha situação, preciso de uma exceção de cancelamento de tarefa para continuar borbulhando para que o código upstream possa detectar o cancelamento do usuário, portanto, preciso capturar a exceção apenas se for realmente o tempo limite - é por isso que estou usando when para filtrar a captura. Usar sua solução de verificação do token pode resultar em um OperationCanceledException não tratado passando o filtro e borbulhando e travando o programa, porque alguém cancelando a operação upstream estará esperando que um TaskCanceledException seja lançado, não um OperationCanceledException .

Se o código que chama o método HttpClient deve lidar com o tempo limite e o cancelamento da tarefa, a distinção entre tempo limite e cancelamento do usuário quando a condição de corrida acontece pode não ser importante para você, dependendo da situação... mas se você estiver escrevendo código de nível de biblioteca e precisar deixar o cancelamento de tarefas borbulhar para ser tratado em um nível mais alto, isso definitivamente fará a diferença.

Se o que você está dizendo é verdade e os tempos limite lançam diferentes tipos de exceção, dependendo do contexto de chamada, isso definitivamente parece um problema para mim que precisa ser resolvido. Não há nenhuma maneira que é ou deveria ser o comportamento pretendido.

Interessante sobre a possível condição de corrida. Eu não tinha considerado isso. Felizmente, para o meu caso de uso, não seria um grande problema.

Quanto às variações de exceção, preciso encontrar algum tempo para simplificar o middleware em que estou trabalhando para ver se ele é consistente com o que vi algumas vezes durante a depuração. As estúpidas chamadas de pré-carregamento da omnibox do Chrome foram onde eu vi no meu middleware quando o tempo estava certo. Acho que posso criar um exemplo simples para provar isso definitivamente de uma forma ou de outra.

Ainda não estou claro sobre qual deve ser a solução alternativa correta.

Minha solução é a mais correta em situações em que os tempos limite de http lançam OperationCanceledException. Não consigo reproduzir a situação em que TaskCanceledException é lançada e ninguém forneceu uma maneira de reproduzir esse comportamento, então digo teste seu ambiente e, se corresponder ao meu, essa é a solução correta.

Se em alguns contextos de execução uma TaskCanceledException for lançada, minha solução falhará como uma solução de propósito geral para esses contextos e a outra solução proposta é a “melhor” solução, mas contém uma condição de corrida que pode ou não ser importante para sua situação, isso é para você avaliar.

Eu adoraria ouvir mais detalhes de alguém que está alegando que está recebendo TaskCanceledExceptions, porque isso seria claramente um grande bug. Estou um pouco cético de que isso esteja realmente acontecendo.

@mikernet se TaskCanceledException ou OperationCanceledException é lançado é amplamente irrelevante. Esse é um detalhe de implementação em que você não deveria confiar. O que você pode fazer é pegar OperationException (que também pegará TaskCanceledException ), e verificar se o token de cancelamento que você passou foi cancelado, conforme descrito no comentário de Hobray (https://github.com/ dotnet/corefx/issues/20296#issuecomment-449510983).

@thomaslevesque Eu já abordei por que essa abordagem é um problema. Sem abordar isso, seu comentário não é muito útil - é apenas refazer as declarações antes de ingressar na conversão sem abordar o problema que descrevi.

Quanto a "isso é um detalhe de implementação" - sim, mas infelizmente tenho que confiar nele porque, caso contrário, não há uma boa maneira de determinar se a tarefa foi realmente cancelada ou expirou. Deve haver 100% de uma maneira de determinar a partir da exceção qual desses dois cenários ocorreu para evitar a condição de corrida descrita. OperationCanceledException é apenas uma má escolha por causa disso... já existe um TimeoutException na estrutura que o torna um candidato melhor.

Exceções para tarefas assíncronas não devem depender da verificação de dados em outro objeto que também pode ser modificado de forma assíncrona para determinar a causa da exceção se um caso de uso comum for tomar uma decisão de ramificação com base na causa da exceção. Isso é implorar por problemas de condição de corrida.

Além disso, em termos do tipo de exceção ser um detalhe de implementação... eu realmente não acho que isso seja verdade para exceções, pelo menos não em termos de práticas de documentação .NET bem estabelecidas. Cite um outro exemplo no .NET Framework onde a documentação diz "lança exceção X quando Y acontece" e o framework realmente lança uma subclasse publicamente acessível dessa exceção, sem falar em uma subclasse publicamente acessível que é usada para um caso de exceção completamente diferente em o mesmo método ! Isso parece uma forma ruim e eu nunca me deparei com isso antes em meus 15 anos de programação .NET.

Independentemente disso, TaskCanceledException deve ser reservado para o cancelamento de tarefas iniciadas pelo usuário, não para tempos limite - como eu disse, já temos uma exceção para isso. As bibliotecas devem capturar quaisquer cancelamentos de tarefas internas e lançar exceções mais apropriadas. Manipuladores de exceção para cancelamentos de tarefas que foram iniciados com tokens de cancelamento de tarefa não devem ser responsáveis ​​por lidar com outros problemas não relacionados que possam ocorrer durante a operação assíncrona.

@mikernet desculpe, perdi essa parte da discussão. Sim, há uma condição de corrida que eu não tinha pensado. Mas pode não ser um problema tão grande quanto parece... A condição de corrida ocorre se o token de cancelamento for sinalizado após o tempo limite ocorrer, mas antes de você verificar o estado do token, certo? Nesse caso, realmente não importa se ocorreu um tempo limite se a operação estiver sendo cancelada de qualquer maneira. Então você deixará um OperationCanceledException borbulhar, deixando o chamador acreditar que a operação foi cancelada com sucesso, o que é uma mentira, pois na verdade ocorreu um tempo limite, mas isso não importa para o chamador, pois ele queria abortar a operação de qualquer maneira.

Deve haver 100% de uma maneira de determinar a partir da exceção qual desses dois cenários ocorreu para evitar a condição de corrida descrita. OperationCanceledException é apenas uma má escolha por causa disso... já existe um TimeoutException na estrutura que o torna um candidato melhor.

Eu concordo completamente com isso, o comportamento atual é claramente errado. Na minha opinião, a melhor opção para corrigi-lo é a opção 1 do comentário de @karelz . Sim, é uma mudança importante, mas suspeito que a maioria das bases de código não está lidando com esse cenário corretamente de qualquer maneira, então não piorará as coisas.

@thomaslevesque Bem, sim, mas eu meio que abordei isso. Pode ou não ser importante para algumas aplicações. No meu é embora. O usuário do meu método de biblioteca não está esperando OperationCanceledException quando cancela uma tarefa - eles estão esperando TaskCanceledException e devem ser capazes de pegar isso e estar "seguros". Se eu verificar o token de cancelamento, ver que ele foi cancelado e passar a exceção, acabei de passar uma exceção que não será capturada pelo manipulador e travará o programa. Meu método não deve lançar em tempos limite.

Claro, existem maneiras de contornar isso ... eu posso simplesmente lançar um novo TaskCanceledException empacotado, mas então eu perco o rastreamento de pilha de nível superior, e os tempos limite de HTTP são tratados de maneira muito diferente dos cancelamentos de tarefas no meu serviço - os tempos limite de HTTP vão em um cache por um longo tempo, enquanto os cancelamentos de tarefas não. Eu não quero perder o cache de tempo limite real devido à condição de corrida.

Acho que poderia pegar OperationCanceledException e verificar o token de cancelamento - se ele foi cancelado, verifique também o tipo de exceção para ter certeza de que é TaskCanceledException antes de relançar para evitar que todo o programa falhe. Se alguma dessas condições for falsa, deve haver um tempo limite. O único problema que resta nesse caso é a condição de corrida que é sutil, mas quando você tem milhares de solicitações HTTP acontecendo por minuto em um serviço muito carregado com tempos limite apertados, isso acontece.

De qualquer forma, isso é hacky e, como você indicou, deve ser resolvido de alguma forma.

(A condição de corrida só acontecerá em ambientes onde TaskCanceledException é lançado em vez de OperationCanceledException , onde quer que seja, mas pelo menos a solução está a salvo de borbulhar um tipo de exceção inesperado e não confiar nos detalhes da implementação para continuar funcionando razoavelmente bem se os detalhes da implementação forem alterados).

A exceção de tempo limite lançada por HttpClient em caso de tempo limite http não deve ser do tipo System.Net.HttpRequestException (ou um descendente?) em vez de System.TimeoutException genérico? Por exemplo, o bom e velho WebClient lança WebException com uma propriedade Status que pode ser definida como WebExceptionStatus.Timeout .

Infelizmente, não poderemos terminar isso para 3.0. Movendo-se para o Futuro.
Ainda está no topo da nossa lista de pendências, mas realisticamente não poderemos fazer isso para 3.0 :(.

@karelz oh não :( Eu ia apenas repostar dizendo como me deparei com isso novamente na segunda empresa consecutiva. Quanto tempo levaria para enviar um PR para 3.0 se eu quisesse resolver isso sozinho? Cheers

Estou tendo dificuldade em rastrear exatamente o que está acontecendo no monte de comentários acima com pessoas relatando diferenças entre obter TaskCancelledException e OperationCancelledException dependendo do contexto, alguém da MSFT pode esclarecer isso?

Curta este comentário de @mikernet :

Como em um bloco catch que captura TaskCanceledException não captura a exceção, ele precisa especificamente de um bloco catch para uma OperationCanceledException.

cc @davidsh @stephentoub talvez?

TaskCanceledException deriva de OperationCanceledException e carrega um pouco de informação adicional. Basta pegar OperationCanceledException.

@karelz a opção 1 que você postou originalmente, apenas fazendo com que ela lance uma exceção de tempo limite, seria completamente intragável?

Coisas como StackExchange.Redis lançam uma exceção derivada da exceção Timeout, é por isso que seria bom apenas capturar a exceção Timeout para HttpClient...

Eu entendo @stephentoub dada a herança, apenas este comentário abaixo de @mikernet está me deixando louco... Esse comportamento que ele descreve teria que ser algum tipo de bug?? Mas se for verdade, influenciaria a correção ou solução alternativa…

"um bloco catch que captura TaskCanceledException não captura a exceção, ele precisa especificamente de um bloco catch para um OperationCanceledException"

Ou você está dizendo que alguns lugares lança um OperationEx e outros lugares um TaskEx? Como talvez as pessoas estejam vendo algo vindo do middleware ASP e pensando que é do HttpClient?

apenas este comentário abaixo do @mikernet está me deixando louco... Esse comportamento que ele descreve teria que ser algum tipo de bug??

Por que isso está "explodindo sua mente"? Se a exceção B derivar da exceção A, um bloco catch para B não capturará uma exceção que seja concretamente A, por exemplo, se eu tiver um bloco catch para ArgumentNullException, ele não capturará " throw new ArgumentException(...)".

Ou você está dizendo que alguns lugares lança um OperationEx e outros lugares um TaskEx?

Direito. Em geral, o código deve assumir apenas OCE; do jeito que as coisas são escritas, algum código pode acabar lançando o TCE derivado, por exemplo, uma tarefa concluída via TaskCompletionSource.TrySetCanceled produzirá uma TaskCanceledException.

Obrigado Stephen, pelos sons das coisas no tópico várias pessoas ficaram com a impressão de que o TaskCE estava sendo jogado em todos os lugares, o que faria com que o comportamento mencionado pelo mikernet não fizesse sentido ... lógica de observação :)

@stephentoub , esta orientação está documentada em algum lugar? Ping @guardrex

Em geral, o código deve assumir apenas OCE ...

Eu trabalho o conjunto de documentos ASP.NET nos dias de hoje. Abra um problema para os gatos dotnet/docs...

https://github.com/dotnet/docs/issues

É possível reiniciar esta conversa onde havia algumas propostas concretas com @karelz e @davidsh ? Parece que alguém (s) que possui a propriedade do modelo de API HttpClient precisa tomar decisões sobre a direção a seguir. Houve alguma discussão sobre exceções de nível superior sempre sendo HttpRequestException (além de cancelamentos explícitos?), misturado com algum tópico de longo prazo sobre um refatoramento maior para modificar HttpRequestion para incluir uma nova propriedade enum semelhante a WebExceptionStatus ...

Parece que todo esse problema está ficando complicado em relação à mudança de código real necessária. Existem outras pessoas que têm participação/propriedade no modelo de API HttpClient que devem ser colocadas em loop para tomar algumas decisões? Muito obrigado a todos!

Estou tendo o mesmo problema aqui - no entanto, cancellationToken.IsCancellationRequested está retornando true :

    public class TimeoutHandler : DelegatingHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            try
            {
                var response = await base.SendAsync(request, cancellationToken);
                response.EnsureSuccessStatusCode();

                return response;
            }
            catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
            {
                throw new TimeoutException("Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.");
            }
        }

Estou forçando um tempo limite. A exceção levantada é TaskCancelledException com IOException como exceção interna:

System.Threading.Tasks.TaskCanceledException: The operation was canceled. ---> System.IO.IOException: Unable to read data from the transport connection: A operação de E/S foi anulada devido a uma saída de thread ou a uma requisição de aplicativo. ---> System.Net.Sockets.SocketException: A operação de E/S foi anulada devido a uma saída de thread ou a uma requisição de aplicativo
   --- End of inner exception stack trace ---
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error)
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.GetResult(Int16 token)
   at System.Net.Http.HttpConnection.FillAsync()
   at System.Net.Http.HttpConnection.ReadNextResponseHeaderLineAsync(Boolean foldedHeadersAllowed)
   at System.Threading.Tasks.ValueTask`1.get_Result()
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   --- End of inner exception stack trace ---
   at System.Net.Http.HttpConnection.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithNtConnectionAuthAsync(HttpConnection connection, HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
   at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)

...

(Desculpe pelo texto de exceção traduzido - aparentemente alguém pensou que seria uma boa ideia trazer isso para o .NET Core também)

Como você pode ver, eu esperava verificar IsCancellationRequest para solucionar esse problema, como outros sugeriram, mas mesmo em tempos limite está retornando true .

Atualmente estou pensando em implementar um timer para verificar se o timeout configurado passou. Mas, obviamente, eu quero morrer , esta é uma solução horrível.

Olá @FelipeTaveira ,

Sua abordagem não funciona, porque o cancelamento é tratado a montante de seu TimeoutHandler , pelo próprio HttpClient . Seu manipulador recebe um token de cancelamento que é cancelado pelo HttpClient quando ocorre um Timeout . Portanto, verificar se o token de cancelamento não foi cancelado funciona apenas no código que está chamando HttpClient , não no código chamado por HttpClient .

A maneira de fazê-lo funcionar com um manipulador personalizado é desabilitar o HttpClient de Timeout definindo-o Timeout.InfiniteTimeSpan e controlando o comportamento de tempo limite em seu próprio manipulador, como mostrado neste artigo (código completo aqui )

@thomaslevesque Ah, obrigado! Eu li seu código e pensei que essa parte era sobre o suporte à configuração de tempo limite por solicitação e não havia notado que também era necessário oferecer suporte à coisa TimeoutException .

Espero que isso seja abordado no futuro.

Pelo que vale a pena, também acho que TaskCanceledException sendo jogado em tempos limite é confuso.

Posso atestar que TaskCanceledExceptions foi uma grande fonte de confusão ao lidar com problemas de produção no meu trabalho anterior. Geralmente, agrupamos chamadas para HttpClient e lançamos TimeoutExceptions quando apropriado.

@eiriktsarpalis obrigado. Planejamos resolvê-lo em 5.0, para que você possa buscá-lo nesse prazo, se quiser, agora que se juntou ao nosso time 😇.

1 ano? 😵 Onde está aquela agilidade prometida do .NET Core?

Julgar a agilidade de um projeto inteiro com base em uma questão é um pouco... injusto.
Sim, é um pouco embaraçoso, ainda temos esse problema, no entanto, é difícil de corrigir (com impacto de compatibilidade) e havia apenas objetivos de negócios de maior prioridade a serem abordados primeiro (em rede) - veja as mudanças nos marcos ao longo da história desta questão.

BTW: Como em muitos outros projetos OSS, não há SLA ou ETAs ou promessas de quando o problema será corrigido. Tudo depende de prioridades, outros trabalhos, dificuldade e número de especialistas capazes de resolver o problema.

Julgar a agilidade de um projeto inteiro com base em uma questão é um pouco... injusto.

Bem, foi uma pergunta honesta. Já se passaram dois anos e quando vi que vai demorar mais um fiquei um pouco surpreso. Sem querer ofender 🙂. Eu sei que, para corrigir esse problema, havia algumas perguntas que precisavam ser respondidas primeiro.

Em um mundo de compromissos... eu não gosto disso, mas aqui está outra opção para mastigar:

Poderíamos continuar jogando TaskCanceledException , mas definir seu CancellationToken como um valor de sentinela.

c# catch(TaskCanceledException ex) when(ex.CancellationToken == HttpClient.TimeoutToken) { }

E outra: jogue um novo HttpTimeoutException que deriva de TaskCanceledException .

E outra: jogue um novo HttpTimeoutException que deriva de TaskCanceledException .

Acho que isso resolveria todas as preocupações. Isso facilita a captura específica de um tempo limite e ainda é um TaskCanceledException , portanto, o código que atualmente captura essa exceção também captura a nova (sem alteração de interrupção).

E outro: lançar um novo HttpTimeoutException que deriva de TaskCanceledException.

Parece bastante feio, mas provavelmente um bom compromisso.

E outra: jogue um novo HttpTimeoutException que deriva de TaskCanceledException .

Acho que isso resolveria todas as preocupações. Isso facilita a captura específica de um tempo limite e ainda é um TaskCanceledException , portanto, o código que atualmente captura essa exceção também captura a nova (sem alteração de interrupção).

Bem, _poderia_ haver pessoas por aí escrevendo código assim:

try
{
  // ...
}
catch (Exception ex) when (ex.GetType() == typeof(TaskCanceledException))
{
}

Portanto, isso seria tecnicamente uma mudança de ruptura.

Também acho que tem um pouco do pior dos dois mundos. Nós ainda não conseguiremos um TimeoutException geral ao mesmo tempo em que temos uma mudança de última hora.

pode haver pessoas por aí escrevendo código assim, então isso seria tecnicamente uma mudança inovadora

FWIW, praticamente todas as mudanças estão possivelmente quebrando, então para avançar em qualquer coisa, é preciso traçar uma linha em algum lugar. No corefx, não consideramos retornar ou lançar um tipo mais derivado para quebrar.
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/breaking-change-rules.md#exceptions

Em um mundo de compromissos... eu não gosto disso, mas aqui está outra opção para mastigar:

Poderíamos continuar jogando TaskCanceledException , mas definir seu CancellationToken como um valor de sentinela.

catch(TaskCanceledException ex) when(ex.CancellationToken == HttpClient.TimeoutToken)
{
}

Eu realmente gosto deste. Existe alguma outra desvantagem para ele além de ser ""feio""? O que é ex.CancellationToken hoje quando acontece um tempo limite?

Existe alguma outra desvantagem para ele além de ser ""feio""

Seria muito fácil para alguém pensar que TimeoutToken pode ser repassado e usado para sinalizar o cancelamento quando ocorre um tempo limite, já que esse é o design usado em outros lugares para essas coisas.

Eu realmente gosto deste. Existe alguma outra desvantagem para ele além de ser ""feio""? O que é ex.CancellationToken hoje quando acontece um tempo limite?

É muito menos intuitivo do que capturar um tipo de exceção específico, que pode ser mencionado explicitamente na documentação.

Estou acostumado a considerar um OperationCanceledException e tipos derivados para significar uma execução interrompida, não um erro de aplicativo. A exceção HttpClient lançada no tempo limite geralmente é um erro - houve uma chamada para uma API HTTP e ela não estava disponível. Esse comportamento leva a complicações durante o processamento de exceções em um nível mais alto, por exemplo, no middleware de pipeline ASP.NET Core, pois esse código precisa saber que existem dois tipos de OperationCanceledException , error e not-error, ou força você a envolver cada chamada HttpClient em um bloco throw-catch.

E outro: lançar um novo HttpTimeoutException que deriva de TaskCanceledException.

Parece bastante feio, mas provavelmente um bom compromisso.

É realmente tão feio, dadas as restrições de design envolvidas e as considerações de compatibilidade com versões anteriores? Não sei se alguém neste tópico propôs uma solução obviamente superior que tenha zero desvantagens (seja uso ou pureza de design, etc.)

Queremos analisar isso mais profundamente do que apenas alterar o tipo de exceção. Há também uma questão de longa data de "onde fizemos o tempo limite?" -- por exemplo, é possível que uma solicitação atinja o tempo limite sem nunca ter atingido o fio -- que precisamos analisar, e uma solução deve ser compatível com a direção em que vamos.

Queremos analisar isso mais profundamente do que apenas alterar o tipo de exceção. Há também uma questão de longa data de "onde fizemos o tempo limite?" -- por exemplo, é possível que um pedido atinja o tempo limite sem nunca ter atingido o fio -- que precisamos analisar, e uma solução deve ser compatível com a direção em que vamos.

Isso é realmente algo que você precisa saber? Quer dizer, você vai lidar com o erro de forma diferente dependendo de onde ele atingiu o tempo limite? Para mim, apenas saber que ocorreu um tempo limite geralmente é suficiente.
Não me importo se você quiser fazer mais, só não quero que essa mudança perca o trem .NET 5 por causa de requisitos adicionais :wink:

Acho que o problema que o @scalablecory levantou é digno de nota, mas concordo que não deve impedir que isso seja corrigido mais cedo. Você pode decidir sobre um mecanicismo para permitir que o motivo do tempo limite seja avaliado, mas por favor não demore para corrigir o problema da exceção para isso... você sempre pode adicionar uma propriedade Reason à exceção mais tarde.

Concordo com @thomaslevesque; Eu não ouvi isso como um grande problema antes. De onde aquilo está vindo?

Esse é um caso de depuração de nicho, em que não há problema em investigar o motivo para exigir um pouco de ação explícita, enquanto para o desenvolvedor geral eles só precisam saber / se preocupar em lidar com uma única coisa.

Queremos analisar isso mais profundamente do que apenas alterar o tipo de exceção. Há também uma questão de longa data de "onde fizemos o tempo limite?" -- por exemplo, é possível que um pedido atinja o tempo limite sem nunca ter atingido o fio -- que precisamos analisar, e uma solução deve ser compatível com a direção em que vamos.

Presumo que a implicação aqui é que deve ser responsabilidade de cada HttpMessageHandler lançar o tipo de exceção de tempo limite correto, em vez de HttpClient consertar as coisas.

Presumo que a implicação aqui é que deve ser responsabilidade de cada HttpMessageHandler lançar o tipo de exceção de tempo limite correto, em vez de HttpClient consertar as coisas.

Não vejo como isso é possível com a abstração definida hoje. HttpMessageHandler não tem conhecimento do tempo limite do HttpClient; no que diz respeito a um HttpMessageHandler, ele apenas recebe um token de cancelamento, que pode ter o cancelamento solicitado por vários motivos, incluindo o token de cancelamento do chamador sendo sinalizado, os CancelPendingRequests do HttpClient sendo chamados, o tempo limite imposto do HttpClient expirando etc.

Isso é realmente algo que você precisa saber? Quer dizer, você vai lidar com o erro de forma diferente dependendo de onde ele atingiu o tempo limite? Para mim, apenas saber que ocorreu um tempo limite geralmente é suficiente.

É útil saber se o servidor pode ter processado sua solicitação. Também é ótimo para diagnóstico - um servidor remoto não atinge o tempo limite, mas os tempos limite de relatório do cliente são uma experiência frustrante.

@davidsh pode ter mais informações.

Concordo com @thomaslevesque; Eu não ouvi isso como um grande problema antes. De onde aquilo está vindo?

Foi um ponto levantado durante uma recente reunião da NCL. Pode ser que decidamos que isso não é importante o suficiente para expor ou deixar as coisas adiadas, mas vale a pena uma discussão rápida primeiro.

@dotnet/ncl @stephentoub Vamos colocar isso na cama. Sabemos que tudo o que fizermos será sutilmente quebrado. Esta parece ser a opção menos pior. Proponho avançar com:

jogue um novo HttpTimeoutException que deriva de TaskCanceledException .

Objeções?

lançar um novo HttpTimeoutException que deriva de TaskCanceledException.

Como fazemos isso na prática? Provavelmente significa que temos que limpar nosso código e encontrar todos os lugares que chamam CancellationToken.ThrowIfCancellationRequested e, em vez disso, convertê-lo para:

c# if (token.IsCancellationRequested) throw new HttpTimeoutException(EXTRASTUFF, token);

bem como lugares explícitos que podemos estar chamando OperationCancelledException também.

Existe alguma opção que não exija passar por todo esse código relacionado de qualquer maneira?

Existe alguma opção que não exija passar por todo esse código relacionado de qualquer maneira?

Provavelmente não, mas pode haver lugares que não controlamos (ainda não temos certeza) e, portanto, precisaremos potencialmente pegar um TaskCancelledException/OperationCancelledException aleatório e relançar o novo HttpTimeoutException.

Provavelmente significa que temos que limpar nosso código e encontrar todos os lugares que chamam CancellationToken.ThrowIfCancellationRequested e, em vez disso, convertê-lo para:

Existe alguma opção que não exija passar por todo esse código relacionado de qualquer maneira?

Não exatamente.

No que diz respeito aos manipuladores, tudo o que eles recebem é um CancellationToken: eles não sabem e não se importam de onde esse token veio, quem o produziu ou por que ele pode ter solicitado o cancelamento. Tudo o que eles sabem é que o cancelamento em algum momento pode ser solicitado e lançam uma exceção de cancelamento em resposta. Então, não há nada a fazer aqui nos manipuladores.

É o próprio HttpClient que está criando o CancellationTokenSource relevante e definindo um tempo limite nele:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L488
O tempo limite é inteiramente invenção do próprio HttpClient, e só ele sabe se gostaria de tratar uma exceção de cancelamento que surgiu como sendo especial.

Isso significa que o código referenciado acima precisaria ser refatorado para rastrear o tempo limite separadamente, como um CancellationTokenSource separado ou, mais provavelmente, manipulando a lógica de tempo limite por conta própria e, além de cancelar o CTS, também definindo um sinalizador que pode então verifique também. Isso provavelmente não será pago para jogar, pois adicionaremos alocações adicionais aqui, mas do ponto de vista do código é relativamente simples.

Então em lugares no HttpClient como:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L536 -L541
e
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L562 -L566
quando captura uma exceção, seria necessário cancelar em caso especial, para verificar se o tempo limite expirou e, se tiver, assumir que foi a causa do cancelamento e lançar uma nova exceção do tipo desejado.

seria necessário um cancelamento em caso especial, para verificar se o tempo limite expirou, e se expirou, assumir que foi a causa do cancelamento e lançar uma nova exceção do tipo desejado.

Como isso funciona se a propriedade HttpClient.Timeout não estiver envolvida? Talvez esteja definido como "infinito". E quanto aos tokens de cancelamento passados ​​diretamente para as APIs HttpClient (GetAsync, SendAync etc.)? Presumo que funcionaria da mesma forma?

Como isso funciona se a propriedade HttpClient.Timeout não estiver envolvida?

Não entendi a pergunta... esse é o único tempo limite de que estamos falando. Se estiver definido como Infinito, o cancelamento nunca será causado por esse tempo limite e não haverá nada a fazer. Já temos um caso especial Timeout == Infinite e continuaríamos a:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L481

E quanto aos tokens de cancelamento passados ​​diretamente para as APIs HttpClient (GetAsync, SendAync etc.)? Presumo que funcionaria da mesma forma?

Combinamos qualquer token que seja passado com um de nossa própria criação:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L485
Nós rastrearíamos nosso próprio tratamento de HttpClient.Timeout, conforme mencionado em meus comentários anteriores. Se o chamador optar por passar um token que configurou com um tempo limite, tratá-lo especialmente depende dele: no que nos diz respeito, é apenas uma solicitação de cancelamento.

Objeções?

Por que introduzir um novo tipo de exceção derivado que "compete" com o TimeoutException existente em vez de, digamos, lançar uma exceção de cancelamento que envolve um TimeoutException como a exceção interna? Se o objetivo for permitir que um consumidor determine programaticamente se o cancelamento ocorreu devido a um tempo limite, isso seria suficiente? Parece comunicar a mesma informação, sem precisar de nova área de superfície.

Por que introduzir um novo tipo de exceção derivado que "compete" com o TimeoutException existente em vez de, digamos, lançar uma exceção de cancelamento que envolve um TimeoutException como a exceção interna

Por conveniência, principalmente. É mais fácil e intuitivo escrever

catch(HttpTimeoutException)

que

catch(OperationCanceledException ex) when (ex.InnerException is TimeoutException)

Dito isto, de qualquer forma é bom para mim, desde que seja razoavelmente fácil identificar um tempo limite.

digamos, lançando uma exceção de cancelamento que envolve um TimeoutException como a exceção interna?

Acho que é um bom compromisso. Isso ajudaria a desambiguar os logs de exceção, que eram minha principal fonte de aborrecimento ao lidar com esses tipos de tempo limite. Provavelmente também comunica melhor o fato de que este é o HttpClient cancelando uma solicitação, em vez de um tempo limite sendo gerado pelo manipulador subjacente. Além disso, não há alterações de quebra.

Triagem:

  • Atualizaremos este cenário para ter InnerException de TimeoutException com uma mensagem para registro/diagnóstico que facilmente distingue o cancelamento como sendo causado pelo acionamento do tempo limite.
  • Atualizaremos a documentação com orientações sobre maneiras de inspecionar programaticamente para diferenciar o cancelamento: verificando TimeoutException , usando um Timeout infinito e passando um CancellationToken com um período de tempo limite etc.

Reconhecemos que qualquer solução aqui envolve algum compromisso e acreditamos que isso fornecerá um bom nível de compatibilidade com o código existente.

Por que introduzir um novo tipo de exceção derivado que "compete" com o TimeoutException existente em vez de, digamos, lançar uma exceção de cancelamento que envolve um TimeoutException como a exceção interna

Por conveniência, principalmente. É mais fácil e intuitivo escrever

catch(HttpTimeoutException)

que

catch(OperationCanceledException ex) when (ex.InnerException is TimeoutException)

Dito isto, de qualquer forma é bom para mim, desde que seja razoavelmente fácil identificar um tempo limite.

@scalablecory Estou chateado por ter perdido a discussão por algumas horas - minha experiência em comunicar esse tipo de coisa com desenvolvedores LoB .NET em geral é que capturar um HttpTimeoutException derivado é muito mais fácil para as pessoas entenderem/adotarem do que ter que lembre-se de usar um filtro de exceção em uma exceção interna. Nem todo mundo sabe o que é um filtro de exceção.

Ao mesmo tempo, reconheço totalmente que este é um daqueles cenários em que não há uma solução clara vencedora/sem compromisso, como você disse.

Concordamos nesse ponto @ericsampson , mas isso seria uma mudança decisiva para muitos usuários existentes. Como @scalablecory mencionou, comprometemo-nos a fazer melhorias, limitando os danos de compatibilidade.

Quando penso nas exceções, em geral, considero a abordagem de encapsulamento melhor. A exceção derivada de OperationCanceledException torna essa exceção disponível apenas no uso assíncrono. E mesmo é apertado para tarefas. Como vimos no passado houve várias evoluções de abordagens assíncronas e considero task como um detalhe de implementação porém TimeoutException, por exemplo, faz parte do conceito de socket que não vai mudar.

Concordamos nesse ponto @ericsampson , mas isso seria uma mudança decisiva para muitos usuários existentes. Como @scalablecory mencionou, comprometemo-nos a fazer melhorias, limitando os danos de compatibilidade.

Embora eu não discorde necessariamente da opção de encapsulamento, acho que vale a pena ressaltar que - pelo que entendi - a questão não é que a opção derivada seja uma alteração de quebra, mas que o encapsulamento seja mais simples. Como @stephentoub mencionou anteriormente na discussão, lançar uma exceção derivada não é considerado uma alteração importante pelas diretrizes do corefx.

A derivação de HttpTimeoutException de TaskCanceledException viola o relacionamento “é um” entre essas duas exceções. Ele ainda fará o código de chamada para pensar que o cancelamento aconteceu, a menos que ele manipule especificamente HttpTimeoutException. Este é basicamente o mesmo tratamento que temos que fazer no momento em que verificamos se o token de cancelamento está sendo cancelado para determinar se é um tempo limite ou não. Além disso, ter duas exceções de tempo limite na biblioteca principal é confuso.
Adicionar TimeoutException como uma exceção interna a TaskCanceledException não fornecerá nenhuma informação adicional na maioria dos casos, pois já podemos verificar o token de cancelamento e determinar se o tempo limite está esgotado ou não.

Eu acho que a única solução sensata é alterar a exceção lançada no tempo limite para TimeoutException. Sim, será uma mudança importante, mas o problema já abrange vários lançamentos principais e as alterações importantes são para o que servem os lançamentos principais.

A documentação das versões existentes pode ser atualizada para indicar que os métodos podem lançar TaskCanceledException e/ou OperationCanceledException (ambos os tipos concretos parecem ser uma possibilidade) quando ocorre um tempo limite? Atualmente a documentação indica que HttpRequestException lida com tempos limite, o que é incorreto e enganoso.

@qidydl esse é o plano... documentará completamente todas as maneiras pelas quais os tempos limite podem se manifestar e como detalhar com eles.

Obrigado @alnikola e @scalablecory e @davidsh e @karelz e todos os outros envolvidos nesta discussão/implementação, eu realmente aprecio isso.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

btecu picture btecu  ·  3Comentários

omajid picture omajid  ·  3Comentários

bencz picture bencz  ·  3Comentários

v0l picture v0l  ·  3Comentários

noahfalk picture noahfalk  ·  3Comentários