Runtime: HttpClient 在超时时抛出 TaskCanceledException

创建于 2017-05-25  ·  119评论  ·  资料来源: dotnet/runtime

在某些情况下,HttpClient 会在超时时抛出 TaskCanceledException。 当服务器负载过重时,我们就会发生这种情况。 我们能够通过增加默认超时来解决问题。 以下 MSDN 论坛帖子抓住了遇到问题的本质: https ://social.msdn.microsoft.com/Forums/en-US/d8d87789-0ac9-4294-84a0-91c9fa27e353/bug-in-httpclientgetasync-should-throw

谢谢

area-System.Net.Http enhancement

最有用的评论

@karelz添加注释作为受此误导性异常影响的另一位客户:)

所有119条评论

@awithy您使用的是哪个版本的 .NET Core?

1.1

无论设计是否正确,根据设计,超时都会抛出 OperationCanceledExceptions(并且 TaskCanceledException 是 OperationCanceledException)。
抄送:@davidsh

我们的团队发现这不直观,但它似乎确实按设计工作。 不幸的是,当我们遇到这个问题时,我们浪费了一些时间来寻找取消的来源。 必须以不同于任务取消的方式处理这种情况是非常难看的(我们创建了自定义处理程序来管理它)。 这似乎也是对取消概念的过度使用。

再次感谢您的快速回复。

似乎是设计使然 + 在过去 6 个月内只有 2 位客户投诉。
关闭。

@karelz添加注释作为受此误导性异常影响的另一位客户:)

感谢您的信息,也请点赞顶帖(问题可以按它排序),谢谢!

无论设计是否正确,根据设计,超时都会抛出 OperationCanceledExceptions

这是一个糟糕的设计IMO。 无法判断请求是否实际被取消(即传递给SendAsync的取消令牌已被取消)或超时是否已过。 在后一种情况下,重试是有意义的,而在前一种情况下则没有。 有一个TimeoutException非常适合这种情况,为什么不使用它呢?

是的,这也让我们的团队陷入了困境。 有一个完整的堆栈溢出线程致力于尝试处理它: https ://stackoverflow.com/questions/10547895/how-can-i-tell-when-httpclient-has-timed-out/32230327#32230327

几天前我发布了一个解决方法: https ://www.thomaslevesque.com/2018/02/25/better-timeout-handling-with-httpclient/

太好了,谢谢。

由于兴趣更大而重新开放 - StackOverflow线程现在有 69 个赞成票。

抄送@dotnet/ncl

任何新闻 ? 仍然发生在 dotnet core 2.0

它在我们的 2.1 后的首要问题列表中。 但它不会在 2.0/2.1 中修复。

当超时发生时,我们有几种选择:

  1. 抛出TimeoutException而不是TaskCanceledException

    • 优点:易于区分超时和运行时的显式取消操作

    • 缺点:技术上的重大变化 - 引发了新类型的异常。

  2. 抛出TaskCanceledException内部异常为TimeoutException

    • 优点:可以在运行时将超时与显式取消操作区分开来。 兼容的异常类型。

    • 开放式问题:是否可以丢弃原始的TaskCanceledException堆栈并抛出一个新堆栈,同时保留InnerException (作为TimeoutException.InnerException )? 还是我们应该保留并只包装原始的TaskCanceledException



      • 要么:新的 TaskCanceledException -> 新的 TimeoutException -> 原始的 TaskCanceledException(原始的 InnerException 可能为空)


      • 或者:新的TaskCanceledException -> 新的TimeoutException -> 原来的TaskCanceledException.InnerException(可能为null)



  3. 抛出TaskCanceledException并带有消息提及超时作为原因

    • 优点:可以从消息/日志中区分超时和显式取消操作

    • 缺点:在运行时无法区分,它是“仅调试”。

    • 开放式问题:与 [2] 中的相同 - 我们应该包装还是替换原始的 TaskCanceledException (并堆栈)

我倾向于选项 [2],丢弃原始TaskCanceledException的原始堆栈。
@stephentoub @davidsh有什么想法吗?

顺便说一句:更改在HttpClient.SendAsync中应该相当简单,我们设置了超时:
https://github.com/dotnet/corefx/blob/30fb78875148665b98748ede3013641734d9bf5c/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L433 -L461

顶级异常应始终是 HttpRequestException。 这是 HttpClient 的标准 API 模型。

其中,作为内部异常,“超时”可能应该是 TimeoutException。 事实上,如果我们将此与其他问题(关于修改 HttpRequestion 以包含类似于 WebExceptionStatus 的新枚举属性)结合起来,那么开发人员只需检查该枚举,而根本不必检查内部异常。

这基本上是选项 1),但继续保留当前的 ​​app-compat API 模型,即来自 HttpClient API 的顶级异常始终是 HttpRequestException。

注意:直接从 Stream API 读取 HTTP 响应流的任何“超时”,例如:

```c#
var client = new HttpClient();
Stream responseStream = await client.GetStreamAsync(url);

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

应该继续抛出 System.IO.IOException 等作为顶级异常(我们实际上在 SocketsHttpHandler 中有一些关于此的错误)。

@davidsh您是否建议即使在用户明确取消的情况下也抛出HttpRequestException ? 那些今天抛出TaskCanceledException看起来还不错。 不确定这是否是我们想要改变的东西......
我可以将客户端超时公开为HttpRequestException而不是TaskCanceledException

@davidsh您是否建议即使在有明确的用户取消的情况下也抛出 HttpRequestException ? 那些今天抛出 TaskCanceledException 似乎很好。 不确定这是否是我们想要改变的……

我需要测试堆栈的不同行为,包括 .NET Framework 以验证确切的当前行为,然后才能回答这个问题。

此外,在某些情况下,我们的一些堆栈会抛出带有 OperationCanceledException 内部异常的 HttpRequestException。 TaskCanceledException 是 OperationCanceledException 的派生类型。

当有明确的用户取消时

我们还需要明确定义什么是“显式”。 例如,是否将 HttpClient.Timeout 属性设置为“显式”? 我想我们说不。 在 CancellationTokenSource 对象上调用 .Cancel() 怎么样(这会影响传入的 CancellationToken 对象)? 这是“明确的”吗? 可能还有其他我们可以想到的例子。

当您设置HttpClient.Timeout时,.NET Framework 也会引发 TaskCanceledException。

如果您无法区分从客户端取消和从实施取消 - 只需检查两者。 在使用客户端令牌创建派生令牌之前:
```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.
}

基本上,您将所有可能的层从客户端令牌过滤到最深的层,直到找到导致取消的令牌。

关于这个制作 2.2 或 3.0 的任何更新? @karelz @davidsh ,抄送@NickCraver 。 我有点希望它只执行选项 1) 抛出 TimeoutException,或者从 HttpRequestException 派生的新 HttpTimeoutException。 谢谢!

关于这个制作 2.2 或 3.0 的任何更新?

2.2 为时已晚,它于上周发布;)

2.2 为时已晚,它于上周发布;)

啊,这就是我休息时会发生的事情;)

虽然有点傻,但我发现的最简单的解决方法是为 OperationCanceledException 嵌套一个 catch 处理程序,然后评估 IsCancellationRequested 的值以确定取消令牌是超时还是实际会话中止的结果。 显然,执行 API 调用的逻辑可能在业务逻辑或服务层中,但为了简单起见,我已经合并了示例:

````csharp
[HttpGet]
公共异步任务Index(CancellationToken cancelToken)
{
尝试
{
//这通常在业务逻辑或服务层中完成,而不是在控制器本身中完成,但为了简单起见,我在这里说明这一点
尝试
{
//对 HttpClient 实例使用 HttpClientFactory
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);
}

}
````

我已经看过其他关于如何更优雅地处理这种情况的文章,但它们都需要深入研究管道等。我意识到这种方法并不完美,我希望未来会对实际超时异常有更好的支持发布。

使用 http 客户端是非常糟糕的。 HttpClient 需要删除并从头开始。

虽然有点傻,但我发现的最简单的解决方法是为 OperationCanceledException 嵌套一个 catch 处理程序,然后评估 IsCancellationRequested 的值以确定取消令牌是超时还是实际会话中止的结果。 显然,执行 API 调用的逻辑可能在业务逻辑或服务层中,但为了简单起见,我已经合并了示例:

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

我已经看过其他关于如何更优雅地处理这种情况的文章,但它们都需要深入研究管道等。我意识到这种方法并不完美,我希望未来会对实际超时异常有更好的支持发布。

我们需要编写这个残暴的代码绝对是疯狂的。 HttpClient 是微软创建的最泄漏的抽象

我同意不包含有关超时的更具体的异常信息似乎相当短视。 对于人们想要以与取消令牌异常不同的方式处理实际超时似乎是一件非常基本的事情。

使用 http 客户端是非常糟糕的。 HttpClient 需要删除并从头开始。\

这不是很有帮助或建设性的反馈@dotnetchris 。 客户和 MSFT 员工都在这里尝试让事情变得更好,这就是为什么像@karelz这样的人仍然在这里倾听客户反馈并努力改进的原因。 没有要求他们在公开场合进行开发,我们不要用消极的态度烧毁他们的善意,否则他们可能会决定拿球回家,这对社区来说会更糟。 谢谢! :)

为什么不只是创建具有预期行为的 HttpClient2(临时名称),并在文档中告知“HttpClient 已过时,请使用 HttpClient2”,并解释了两者之间的区别。

这样就不会在 .NetFramework 中插入重大更改。

HttpClient 不应该基于抛出异常。 互联网主机不可用并不例外。 实际上,考虑到地址的无限排列,这是意料之中的,例外情况是给它一些有用的东西。

我什至在博客上写过 3 年前使用 HttpClient 有多糟糕。 https://dotnetchris.wordpress.com/2015/12/11/working-with-httpclient-is-absurd/

这是一个非常糟糕的设计。

微软有没有人用过HttpClient? 看起来肯定不像。

然后除了这个可怕的 api 表面之外,还有关于它的构造和线程的各种泄漏抽象。 我不敢相信它是可以挽救的,只有全新的才是可行的解决方案。 HttpClient2,HttpClientSlim 都可以。

Http 访问不应该抛出异常。 它应该总是返回一个结果,结果应该能够检查完成、超时、http响应。 它应该被允许抛出的唯一原因是如果你调用EnsureSuccess()那么它可以抛出世界上的任何异常。 但是对于互联网的完全正常行为应该有零控制流处理:主机启动、主机未启动、主机超时。 这些都不是例外。

HttpClient 不应该基于抛出异常。 互联网主机不可用并不例外。 实际上,考虑到地址的无限排列,这是意料之中的,例外情况是给它一些有用的东西。

@dotnetchris ,因为听起来您对这个主题充满热情,为什么不花时间为 HttpClient2 编写您提议的完整 API 并将其作为新问题提交给团队讨论?

我没有费心从更深的技术层面深入研究使 HttpClient 好坏的具体细节,因此我没有任何要添加到 @dotnetchris 的评论的内容。 我绝对不认为批评 MS 开发团队是合适的,但我也理解花费数小时研究看似超级简单的情况的解决方案是多么令人沮丧。 努力理解为什么做出某些决定/改变是一种挫败感,希望 MS 的某个人能够解决这个问题,同时仍然努力完成并交付您的工作。

就我而言,我在 OAuth 过程中调用了一个外部服务来建立用户角色,如果该调用在指定的时间跨度内超时(不幸的是,这种情况发生的次数超出了我的预期),我需要恢复到辅助方法.

确实有无数其他原因,我可以理解为什么有人希望能够区分超时和取消。 为什么对这样的常见场景似乎不存在更好的支持是我难以理解的,我真的希望 MS 团队能够对此进行更多思考。

@karelz需要什么才能按时完成 3.0(某人的 API 提案)?

这与 API 提案无关。 这是关于找到正确的实现。 它在我们的 3.0 待办事项中并且在列表中相当高(在 HTTP/2 和企业场景之后)。 这是我们团队在 3.0 中解决问题的好机会。
当然,如果社区中有人受到激励,我们也会做出贡献。

@karelz在这种情况下社区如何提供帮助? 我提到是否需要 API 提案的原因是,我不确定您的消息开头是“我们有几个选项在发生超时时该怎么办:”如果团队已经从您列出的那些中决定了他们的首选替代方案。 如果您已经确定了一个方向,那么我们可以从那里开始。 谢谢!

从技术上讲,这不是 API 提案问题。 该决定不需要 API 批准。 但是,我同意我们应该就如何暴露差异保持一致——可能会有一些讨论。
我不希望这些选项在实施中彼此有太大的不同——如果社区想要提供帮助,实现任何选项将使我们达到 98%(假设从一个地方抛出异常并且可以以后很容易更改以适应上面列出的 [1]-[3] 选项)。

你好! 我们最近也遇到了这个问题。 只是好奇它是否仍然在您的雷达上 3.0 ?

是的,它仍然在我们的 3.0 列表中——我仍然会给它 90% 的机会,它会在 3.0 中得到解决。

我今天遇到了这个。 我在 HttpClient 上设置了一个超时,它给了我这个

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

我同意其他人的观点,这令人困惑,因为它没有提到超时是问题。

嗯,好吧,我有点困惑。 这个线程中的每个人似乎都在说 HTTP 超时会抛出TaskCanceledException但这不是我得到的......我在 .NET Core 2.1 中得到了OperationCanceledException ......

如果你想为超时抛出一个 OperationCanceledException ,那么我可以解决这个问题,但我只是花了几个小时试图追踪到底发生了什么,因为它没有正确记录:

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

它清楚地表明HttpRequestException被抛出超时......

TaskCanceledException 派生自 OperationCanceledException。

@stephentoub是的,但我没有得到TaskCanceledException ,我得到的是OperationCanceledException

如,捕获TaskCanceledException的 catch 块不会捕获异常,它特别需要OperationCanceledException的 catch 块。

我现在通过过滤异常类型来专门测试 HTTP 超时,我知道它是超时的方式是它是否不是TaskCanceledException

```c#
尝试 {
使用 (var response = await s_httpClient.SendAsync(request, _updateCancellationSource.Token).ConfigureAwait(false))
返回 (int)response.StatusCode < 400;
}
catch (OperationCanceledException ex) when (ex.GetType() != typeof(TaskCanceledException)) {
// HTTP 超时
返回假;
}
捕捉(HttpRequestException){
返回假;
}

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

嗯,好吧,我有点困惑。 这个线程中的每个人似乎都在说 HTTP 超时会抛出TaskCanceledException但这不是我得到的......我在 .NET Core 2.1 中得到了OperationCanceledException ......

如果你想为超时抛出一个 OperationCanceledException ,那么我可以解决这个问题,但我只是花了几个小时试图追踪到底发生了什么,因为它没有正确记录:

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

它清楚地表明HttpRequestException被抛出超时......

您是否通读了整个线程,因为实际上有几个响应引用了 OperationCanceledExecption,包括我提出的代码示例,它显示了一种明确处理超时的方法(不像我想要的那样干净,但它是一种方法)。

@Hobray是的,我有 - 是什么让你认为我没有? 这仍然不能解释这个线程中有多少人在超时时得到TaskCanceledException ,但我没有。

我也看到了你的代码示例,但不幸的是它有一个竞争条件,我不相信你考虑过。 发布我正在使用的代码避免了这个问题,并且不需要访问取消令牌,如果您试图在上游进一步捕获超时,这可能很重要。

@mikernet也许措辞让我失望。 你说得对,我没有考虑过可能的比赛条件。 我意识到这不是一个很棒的解决方案,但如果您想解释我忽略了什么,我想听听。

我注意到的一件事是,您得到的确切异常似乎确实会有所不同,具体取决于超时或取消是否发生在控制器与中间件调用中。 在中间件调用内部,它似乎只是TaskCanceledException 。 不幸的是,我没有时间深入研究 .NET Core 代码库以了解原因,但我注意到了这一点。

@Hobray哈哈,是的,当然-抱歉,我并不是要对比赛条件保密,我只是在旅途中打着最后一条信息时在手机上,这很难解释。

如果在HttpClient超时和您检查取消令牌取消状态的时间点之间启动任务取消,则解决方案中的竞争条件会发生。 在我的情况下,我需要一个任务取消异常来保持冒泡,以便上游代码可以检测到用户取消,因此我需要能够仅在它实际超时时捕获异常 - 这就是我使用when的原因过滤捕获。 使用检查令牌的解决方案可能会导致未处理的OperationCanceledException通过过滤器并冒泡并使程序崩溃,因为取消上游操作的人会期望抛出TaskCanceledException ,而不是一个OperationCanceledException

如果调用HttpClient方法的代码应该同时处理超时和任务取消,那么在竞争条件发生时超时和用户取消之间的区别对您来说可能并不重要,具体取决于具体情况......但是如果您正在编写库级别的代码并且需要让任务取消冒泡以在更高级别进行处理,那么它肯定会有所作为。

如果您说的是真的,并且超时会根据调用上下文抛出不同的异常类型,那么这对我来说绝对是一个需要解决的问题。 没有办法是或应该是预期的行为。

对可能的比赛条件很感兴趣。 我没有考虑到这一点。 幸运的是,对于我的用例来说,这不是什么大问题。

至于异常变化,我需要花一些时间来简化我一直在研究的中间件,看看它是否与我在调试时看到的几次一致。 当时机恰到好处时,愚蠢的 Chrome 多功能框预加载调用一直是我在中间件中看到的地方。 我想我可以创建一个简单的例子来明确地证明它。

我仍然不清楚正确的解决方法应该是什么。

在 http 超时抛出 OperationCanceledException 的情况下,我的解决方案是最正确的。 我无法重现抛出 TaskCanceledException 并且没有人提供重现该行为的方法的情况,所以我说测试您的环境,如果它与我的匹配,那么这就是正确的解决方案。

如果在某些执行上下文中抛出 TaskCanceledException,那么我的解决方案作为这些上下文的通用解决方案失败,而提出的其他解决方案是“最佳”解决方案,但包含对您的情况可能或可能不重要的竞争条件,这是你来评价。

我很想听到有人声称他们正在获得 TaskCanceledExceptions 的更多细节,因为这显然是一个巨大的错误。 我有点怀疑这是否真的发生了。

@mikernet是否抛出TaskCanceledExceptionOperationCanceledException在很大程度上无关紧要。 这是您不应该依赖的实现细节。 您可以做的是捕获OperationException (这也将捕获TaskCanceledException ),并检查您传递的取消令牌是否被取消,如 Hobray 的评论中所述(https://github.com/ dotnet/corefx/issues/20296#issuecomment-449510983)。

@thomaslevesque我已经解决了为什么这种方法是一个问题。 如果不解决这个问题,您的评论就不是很有用 - 它只是在我加入转换之前重新散列语句而没有解决我描述的问题。

至于“这是一个实现细节” - 是的,但不幸的是我必须依赖它,因为否则没有一个好的方法来确定任务是否实际被取消或超时。 应该 100% 有一种方法可以从异常中确定发生了这两种情况中的哪一种,以避免所描述的竞争条件。 OperationCanceledException只是一个糟糕的选择,因为这个......框架中已经有一个TimeoutException可以成为更好的候选人。

异步任务的异常不应该依赖于检查另一个对象中的数据,如果一个常见的用例是根据异常的原因做出分支决策,那么该对象也可以异步修改以确定异常的原因。 那是在乞求比赛条件问题。

此外,就作为实现细节的异常类型而言......我真的不认为这对于异常来说是正确的,至少在完善的 .NET 文档实践方面不是这样。 举一个 .NET Framework 中的另一个示例,其中文档说“当 Y 发生时抛出异常 X”,并且框架实际上抛出了该异常的可公开访问的子类,更不用说用于完全不同的异常情况的可公开访问的子类了同样的方法! 这似乎是一种糟糕的形式,在我 15 年的 .NET 编程中,我从未遇到过这种情况。

无论如何, TaskCanceledException应该保留用于用户发起的任务取消,而不是用于超时——就像我说的那样,我们已经有一个例外。 库应该捕获任何内部任务取消并抛出更合适的异常。 使用任务取消令牌启动的任务取消的异常处理程序不应负责处理异步操作期间可能发生的其他不相关问题。

@mikernet抱歉,我错过了讨论的那一部分。 是的,有一个我没有想到的比赛条件。 但它可能没有看起来那么大的问题......如果在超时发生之后但在检查令牌状态之前发出取消令牌信号,则会发生竞争条件,对吗? 在这种情况下,如果操作被取消,是否发生超时并不重要。 所以你会让OperationCanceledException冒泡,让调用者相信操作已成功取消,这是一个谎言,因为实际上发生了超时,但这对调用者来说并不重要,因为他们想要无论如何都要中止操作。

应该 100% 有一种方法可以从异常中确定发生了这两种情况中的哪一种,以避免所描述的竞争条件。 OperationCanceledException只是一个糟糕的选择,因为这个......框架中已经有一个TimeoutException可以成为更好的候选人。

我完全同意这一点,目前的行为显然是错误的。 在我看来,修复它的最佳选择是@karelz评论中的选项 1。 是的,这是一个重大变化,但我怀疑大多数代码库无论如何都没有正确处理这种情况,所以它不会让事情变得更糟。

@thomaslevesque好吧,是的,但我有点解决这个问题。 它对某些应用程序可能重要也可能不重要。 不过在我的情况下。 我的库方法的用户在取消任务时并不期待OperationCanceledException - 他们期待TaskCanceledException并且他们应该能够抓住它并“安全”。 如果我检查取消令牌,看到它已被取消并传递异常,那么我刚刚传递了一个异常,该异常不会被他们的处理程序捕获并且会使程序崩溃。 我的方法不应该引发超时。

当然,有一些方法可以解决这个问题......我可以抛出一个新包装的TaskCanceledException但随后我丢失了顶层堆栈跟踪,并且 HTTP 超时的处理方式与我的服务中的任务取消有很大不同 - HTTP 超时进入缓存很长时间,而任务取消则不会。 由于竞争条件,我不想错过缓存实际超时。

我想我可以捕获OperationCanceledException并检查取消令牌 - 如果它已被取消,那么还要检查异常的类型以确保在重新抛出之前它是TaskCanceledException以避免整个程序崩溃。 如果这些条件中的任何一个为假,那么它必须是超时。 在这种情况下,唯一剩下的问题是竞争条件是微妙的,但是当你在一个负载很重的服务上每分钟发生数千个 HTTP 请求并设置严格的超时时,它确实会发生。

无论哪种方式,这都是hacky,正如您所指出的,应该以某种方式解决。

(竞争条件只会发生在抛出TaskCanceledException而不是OperationCanceledException的环境中,无论可能在哪里,但至少该解决方案可以安全地避免出现意外的异常类型并且不会依赖实现细节,以便在实现细节发生变化时仍能正常工作)。

HttpClient在http超时的情况下抛出的超时异常不应该是类型System.Net.HttpRequestException (或后代?)而不是通用System.TimeoutException ? 例如,旧的WebClient抛出带有Status属性的WebException ,该属性可以设置为WebExceptionStatus.Timeout

遗憾的是,我们将无法在 3.0 中完成此操作。 走向未来。
它仍然在我们的积压工作中,但实际上我们将无法在 3.0 中做到这一点:(。

@karelz哦不 :( 我只是要重新发布,说我是如何在第二家公司再次遇到这个问题的。如果我想自己解决这个问题,提交 3.0 的 PR 的窗口需要多长时间?干杯

我很难准确地跟踪上面一堆评论中发生的事情,人们根据上下文报告获取 TaskCancelledException 和 OperationCancelledException 之间的差异,MSFT 的任何人都可以说明这一点吗?

喜欢@mikernet的评论:

如,捕获 TaskCanceledException 的 catch 块不会捕获异常,它特别需要 OperationCanceledException 的 catch 块。

cc @davidsh @stephentoub也许?

TaskCanceledException 派生自 OperationCanceledException,并带有一些附加信息。 只需捕获 OperationCanceledException。

@karelz你最初发布的选项 1,只是让它抛出一个超时异常,是完全不受欢迎的吗?

诸如 StackExchange.Redis 之类的东西会抛出一个从 Timeout 异常派生的异常,这就是为什么只捕获 HttpClient 的 Timeout 异常会很好的原因......

我理解@stephentoub给出了继承,只是@mikernet下面的这条评论让我大吃一惊……他描述的这种行为一定是某种错误? 但如果属实,会影响修复或解决方法……

“捕获 TaskCanceledException 的 catch 块不会捕获异常,它特别需要 OperationCanceledException 的 catch 块”

或者你是说有些地方抛出 OperationEx 而其他地方抛出 TaskEx ? 就像人们看到一些来自 ASP 中间件的东西并认为它来自 HttpClient 一样?

只是@mikernet下面的这条评论让我大吃一惊……他描述的这种行为一定是某种错误?

为什么会“让你大吃一惊”? 如果异常 B 派生自异常 A,则 B 的 catch 块将不会捕获具体为 A 的异常,例如,如果我有 ArgumentNullException 的 catch 块,它将不会捕获“throw new ArgumentException(...)”。

或者你是说有些地方抛出 OperationEx 而其他地方抛出 TaskEx ?

对。 一般来说,代码应该只假设 OCE; 按照编写的方式,某些代码可能最终会抛出派生的 TCE,例如,通过 TaskCompletionSource.TrySetCanceled 完成的任务将产生 TaskCanceledException。

谢谢斯蒂芬,从线程中的事情的声音来看,很多人的印象是TaskCE被扔到任何地方,这会使mikernet提到的行为毫无意义......现在你已经澄清了,这使得他的观察逻辑:)

@stephentoub ,此指南是否记录在任何地方? ping @guardrex

一般来说,代码应该只假设 OCE ...

这些天我使用 ASP.NET 文档集。 为 dotnet/docs 猫打开一个问题...

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

是否有可能在与@karelz@davidsh有一些具体建议的情况下重新开始对话? 感觉就像拥有 HttpClient API 模型所有权的人需要就前进的方向做出决定。 有一些关于顶级异常始终是 HttpRequestException 的讨论(除了显式取消??),混合了一些关于更大的重构以修改 HttpRequestion 以包含类似于 WebExceptionStatus 的新枚举属性的长期看似主题......

只是感觉整个问题相对于所需的实际代码更改变得更加棘手。 是否有其他人在 HttpClient API 模型中拥有股份/所有权,应该循环进入以做出某些决定? 非常感谢!

我在这里遇到了同样的问题-但是, cancellationToken.IsCancellationRequested正在返回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.");
            }
        }

我正在强制超时。 引发的异常是TaskCancelledException ,其中IOException作为内部异常:

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)

...

(很抱歉翻译的异常文本——显然有人认为将其带到 .NET Core 也是一个好主意)

如您所见,我希望像其他人建议的那样检查IsCancellationRequest以解决此问题,但即使在超时时它也会返回true

目前我正在考虑实现一个计时器来检查配置的超时是否已经过去。 但是,显然我想死,这是一个可怕的解决方法。

@FelipeTaveira

您的方法不起作用,因为取消是在TimeoutHandler的上游由HttpClient本身处理的。 当发生Timeout时,您的处理程序会收到由HttpClient取消的取消令牌。 因此,检查取消令牌是否未取消仅适用于调用HttpClient的代码,而不适用于HttpClient调用代码。

使其与自定义处理程序一起工作的方法是通过将其设置为Timeout.InfiniteTimeSpan来禁用HttpClientTimeout $ ,并控制您自己的处理程序中的超时行为,如显示在本文中(完整代码在这里

@thomaslevesque哦,谢谢! 我已经阅读了您的代码,并认为这部分是关于支持每个请求的超时配置,但没有注意到也有必要支持TimeoutException的东西。

我希望将来能解决这个问题。

对于它的价值,我还发现在超时时抛出TaskCanceledException令人困惑。

我可以证明,在我之前的工作中处理生产问题时, TaskCanceledExceptions是造成混乱的主要来源。 我们通常会包装对 HttpClient 的调用并在适当的地方抛出 TimeoutExceptions。

@eiriktsarpalis谢谢。 我们计划在 5.0 中解决这个问题,所以如果你愿意,你可以在那个时间范围内拿起它,既然你加入了我们的团队 😇。

1年? 😵 .NET Core 承诺的敏捷性在哪里?

基于一个问题来判断整个项目的敏捷性有点……不公平。
是的,这有点尴尬,我们仍然有这个问题,但是,它很难解决(对兼容性有影响),而且只有更高优先级的业务目标需要首先解决(在网络中)——看看历史上里程碑的变化这个问题的。

顺便说一句:与许多其他 OSS 项目一样,没有 SLA 或 ETA 或关于何时修复问题的承诺。 这一切都取决于优先事项、其他工作、难度和能够解决问题的专家数量。

基于一个问题来判断整个项目的敏捷性有点……不公平。

嗯,这是一个诚实的问题。 已经两年了,当我看到它会再拿一个时,我有点惊讶。 虽然没有冒犯的意思🙂。 我知道为了解决这个问题,有几个问题必须/必须首先回答。

在一个妥协的世界里......我不喜欢它,但这里有另一种选择:

我们可以继续抛出TaskCanceledException ,但将其CancellationToken设置为哨兵值。

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

另一个:抛出一个新的HttpTimeoutException ,它派生自TaskCanceledException

另一个:抛出一个新的HttpTimeoutException ,它派生自TaskCanceledException

我认为这将解决所有问题。 它可以很容易地专门捕获超时,并且它仍然是TaskCanceledException ,因此当前捕获该异常的代码也将捕获新异常(没有重大更改)。

另一个:抛出一个派生自 TaskCanceledException 的新 HttpTimeoutException。

它看起来相当丑陋,但可能是一个很好的妥协。

另一个:抛出一个新的HttpTimeoutException ,它派生自TaskCanceledException

我认为这将解决所有问题。 它可以很容易地专门捕获超时,并且它仍然是TaskCanceledException ,因此当前捕获该异常的代码也将捕获新异常(没有重大更改)。

好吧,可能会有人在写这样的代码:

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

所以这在技术上将是一个突破性的变化。

我也认为它有点两全其美。 我们仍然不会在进行重大更改的同时获得一般的TimeoutException

可能有人在那里编写这样的代码,所以这在技术上将是一个突破性的变化

FWIW,几乎每一个变化都可能打破,所以要在任何事情上取得进展,必须在某处划清界限。 在 corefx 中,我们不认为返回或抛出更派生的类型是破坏性的。
https://github.com/dotnet/corefx/blob/master/Documentation/coding-guidelines/break-change-rules.md#exceptions

在一个妥协的世界里......我不喜欢它,但这里有另一种选择:

我们可以继续抛出TaskCanceledException ,但将其CancellationToken设置为哨兵值。

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

我真的很喜欢这一个。 除了“丑”之外,它还有什么缺点吗? 发生超时时,今天的ex.CancellationToken是多少?

除了“丑”之外,它还有什么缺点吗?

有人很容易认为 TimeoutToken 可以被传递并用于在发生超时时发出取消信号,因为这是在其他地方用于此类事情的设计。

我真的很喜欢这一个。 除了“丑”之外,它还有什么缺点吗? 发生超时时,今天的ex.CancellationToken是什么?

它比捕获特定异常类型要直观得多,可以在文档中明确提及。

我习惯于考虑使用OperationCanceledException和派生类型来表示执行中断,而不是应用程序错误。 HttpClient在超时时抛出的异常通常是一个错误 - 调用了 HTTP API,但它不可用。 这种行为要么在更高级别处理异常时导致复杂化,例如在 ASP.NET Core 管道中间件中,因为此代码必须知道有两种风格OperationCanceledException ,错误和非错误,或者它会强制您将每个HttpClient调用包装在一个 throw-catch 块中。

另一个:抛出一个派生自 TaskCanceledException 的新 HttpTimeoutException。

它看起来相当丑陋,但可能是一个很好的妥协。

考虑到所涉及的设计约束和向后兼容性考虑,它真的那么难看吗? 我不知道该线程中的任何人都提出了一个明显优越的解决方案,该解决方案具有零缺点(无论是使用还是设计纯度等)

我们想要更深入地了解这一点,而不仅仅是更改异常类型。 还有一个长期存在的问题“我们在哪里超时?” ——例如,一个请求有可能在没有命中的情况下超时——我们需要查看,并且解决方案应该与我们去那里的方向兼容。

我们想要更深入地了解这一点,而不仅仅是更改异常类型。 还有一个长期存在的问题“我们在哪里超时?” ——例如,一个请求有可能在没有命中的情况下超时——我们需要查看,并且解决方案应该与我们去那里的方向兼容。

这真的是你需要知道的吗? 我的意思是,您是否会根据超时位置以不同方式处理错误? 对我来说,只要知道发生了超时通常就足够了。
我不介意您是否想做更多,我只是不希望此更改因为附加要求而错过 .NET 5 培训:wink:

我认为@scalablecory提出的问题值得注意,但我同意它不应该阻碍这个问题尽快得到解决。 您可以决定一种机制来允许评估超时原因,但请不要为此延迟修复异常问题...您以后可以随时向异常添加Reason属性。

我同意@thomaslevesque; 我以前没有听说这是一个大问题。 那是从哪里来的?

这是一个相当小众的调试案例,可以深入研究需要一些明确操作的原因,而对于一般开发人员来说,他们只需要知道/关心处理一件事情。

我们想要更深入地了解这一点,而不仅仅是更改异常类型。 还有一个长期存在的问题“我们在哪里超时?” ——例如,一个请求有可能在没有命中的情况下超时——我们需要查看,并且解决方案应该与我们去那里的方向兼容。

我认为这里的含义是每个 HttpMessageHandler 应该负责抛出正确的超时异常类型,而不是 HttpClient 修复问题。

我认为这里的含义是每个 HttpMessageHandler 应该负责抛出正确的超时异常类型,而不是 HttpClient 修复问题。

我看不出今天定义的抽象怎么可能。 HttpMessageHandler 不知道 HttpClient 的 Timeout; 就 HttpMessageHandler 而言,它只是传递了一个取消令牌,该令牌可能出于多种原因请求取消,包括发出调用者的取消令牌信号、调用 HttpClient 的 CancelPendingRequests、HttpClient 的强制超时到期等。

这真的是你需要知道的吗? 我的意思是,您是否会根据超时位置以不同方式处理错误? 对我来说,只要知道发生了超时通常就足够了。

了解服务器是否已处理您的请求很有用。 它也非常适合诊断——远程服务器没有超时,但客户端报告超时是一种令人沮丧的体验。

@davidsh可能有更多的见解。

我同意@thomaslevesque; 我以前没有听说这是一个大问题。 那是从哪里来的?

这是在最近的 NCL 会议上提出的一个观点。 可能我们认为这不足以暴露或延迟事情,但值得先快速讨论一下。

@dotnet/ncl @stephentoub让我们把这个放到床上。 我们知道,我们所做的任何事情都会巧妙地破坏。 这似乎是最不坏的选择。 我建议继续:

抛出一个新的HttpTimeoutException ,它派生自TaskCanceledException

反对?

抛出一个派生自 TaskCanceledException 的新 HttpTimeoutException。

我们如何在实践中做到这一点? 这可能意味着我们必须清理我们的代码并找到所有调用CancellationToken.ThrowIfCancellationRequested的地方,然后将其转换为:

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

除了一个明确的地方,我们实际上也可能调用OperationCancelledException

是否有任何选项不需要遍历所有相关代码?

是否有任何选项不需要遍历所有相关代码?

可能不会,但可能有些地方我们无法控制(还不确定),因此我们需要潜在地抓取随机的 TaskCancelledException/OperationCancelledException 并重新抛出新的 HttpTimeoutException。

这可能意味着我们必须清理我们的代码并找到所有调用 CancellationToken.ThrowIfCancellationRequested 的地方,然后将其转换为:

是否有任何选项不需要遍历所有相关代码?

不完全是。

就处理程序而言,他们收到的只是一个 CancellationToken:他们不知道也不关心该令牌来自哪里,是谁生产的,或者为什么它可能会请求取消。 他们所知道的是,在某些时候可能会要求取消,并且他们会抛出一个取消异常作为响应。 所以,在处理程序中没有什么可做的。

HttpClient 本身正在创建相关的 CancellationTokenSource 并为其设置超时:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L488
超时完全是 HttpClient 自己发明的,只有它自己知道是否愿意将出现的取消异常视为特殊处理。

这意味着需要重构上面引用的代码以单独跟踪超时,或者作为单独的 CancellationTokenSource,或者更有可能通过自己处理超时逻辑,并且除了取消 CTS,还设置一个标志,它可以然后还要检查。 这很可能是非付费游戏,因为我们将在这里添加额外的分配,但从代码的角度来看,它相对简单。

然后在 HttpClient 中的位置,例如:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L536 -L541

https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L562 -L566
当它捕获到异常时,它需要特殊情况取消,以检查超时是否过期,如果已经过期,则假设它是取消的原因并抛出所需类型的新异常。

它需要特殊情况的取消,以检查超时是否过期,如果已经过期,则假设它是取消的原因并抛出所需类型的新异常。

如果不涉及 HttpClient.Timeout 属性,这将如何工作? 也许它被设置为“无限”。 直接传递到 HttpClient API(GetAsync、SendAync 等)的取消令牌怎么样? 我认为这会起作用吗?

如果不涉及 HttpClient.Timeout 属性,这将如何工作?

我不明白这个问题......这是我们在这里谈论的唯一超时。 如果它设置为无限,那么这个超时将永远不会导致取消,并且没有什么可做的。 我们已经特例 Timeout == Infinite 并将继续:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L481

直接传递到 HttpClient API(GetAsync、SendAync 等)的取消令牌怎么样? 我认为这会起作用吗?

我们将传入的任何令牌与我们自己制作的令牌结合起来:
https://github.com/dotnet/corefx/blob/bdd33976fe3713cc3e78b83cf2a1176c70b793be/src/System.Net.Http/src/System/Net/Http/HttpClient.cs#L485
正如我之前的评论中提到的,我们将跟踪我们自己对 HttpClient.Timeout 的处理。 如果调用者选择传入一个他们配置了超时的令牌,那么特殊处理取决于他们:就我们而言,这只是一个取消请求。

反对?

为什么要引入与现有 TimeoutException “竞争”的新派生异常类型,而不是抛出一个将 TimeoutException 包装为内部异常的取消异常? 如果目标是使消费者能够以编程方式确定取消是否是由于超时,这是否足够? 它似乎可以传达相同的信息,而不需要新的表面积。

为什么要引入与现有 TimeoutException “竞争”的新派生异常类型,而不是抛出将 TimeoutException 包装为内部异常的取消异常

主要是为了方便。 写起来更简单、更直观

catch(HttpTimeoutException)

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

话虽如此,任何一种方式对我来说都很好,只要可以相当容易地识别超时。

比如说,抛出一个将 TimeoutException 包装为内部异常的取消异常?

我认为这是一个很好的妥协。 这将有助于消除异常日志的歧义,这是我在处理这类超时时的主要烦恼来源。 它可能也更好地传达了这样一个事实,即这是 HttpClient 取消请求,而不是底层处理程序引发的超时。 此外,没有重大变化。

分流:

  • 我们将更新此场景,使其具有 $ TimeoutExceptionInnerException以及用于记录/诊断的消息,以便轻松区分取消是由超时触发引起的。
  • 我们将更新文档,并提供有关可能以编程方式检查以区分取消的方式的指南:检查TimeoutException ,使用无限Timeout并传递带有超时期限的CancellationToken等。

我们认识到这里的任何解决方案都涉及一些妥协,并相信这将提供与现有代码的良好兼容性。

为什么要引入与现有 TimeoutException “竞争”的新派生异常类型,而不是抛出将 TimeoutException 包装为内部异常的取消异常

主要是为了方便。 写起来更简单、更直观

catch(HttpTimeoutException)

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

话虽如此,任何一种方式对我来说都很好,只要可以相当容易地识别超时。

@scalablecory我很沮丧,我似乎错过了几个小时的讨论 - 我与一般 LoB .NET 开发人员交流这类事情的经验是,捕捉派生的 HttpTimeoutException 比人们更容易理解/采用请记住对内部异常使用异常过滤器。 甚至不是每个人都知道异常过滤器是什么。

同时,我完全认识到,正如您所说,这是没有一个明确的获胜/不妥协解决方案的场景之一。

我们同意@ericsampson的这一点,但这对许多现有用户来说将是重大变化。 正如@scalablecory提到的,我们在限制兼容性损害的同时做出了妥协。

当我考虑异常时,一般来说,我认为包装方法更好。 从 OperationCanceledException 派生异常使得此类异常仅在异步使用中可用。 甚至它对任务也很紧张。 正如我们在过去看到的,异步方法有几次演变,我认为任务是一个实现细节,但是 TimeoutException 是套接字概念的一部分,不会改变。

我们同意@ericsampson的这一点,但这对许多现有用户来说将是重大变化。 正如@scalablecory提到的,我们在限制兼容性损害的同时做出了妥协。

虽然我不一定不同意包装选项,但我认为值得指出的是 - 据我所知 - 问题不在于派生选项是一项重大更改,而是包装选项更简单。 正如@stephentoub在前面讨论中提到的,抛出派生异常不被 corefx 指南视为重大更改。

从 TaskCanceledException 派生 HttpTimeoutException 违反了这两个异常之间的“是”关系。 它仍然会让调用代码认为发生了取消,除非它专门处理 HttpTimeoutException。 这基本上与我们在检查取消令牌是否被取消以确定它是否超时时必须做的处理相同。 此外,核心库中有两个超时异常是令人困惑的。
在大多数情况下,将 TimeoutException 作为内部异常添加到 TaskCanceledException 不会提供任何额外信息,因为我们已经可以检查取消令牌并确定它是否超时。

我认为唯一明智的解决方案是将超时引发的异常更改为 TimeoutException。 是的,这将是一个重大更改,但该问题已经跨越了几个主要版本,而重大更改就是主要版本的用途。

是否可以更新现有版本的文档以指示在发生超时时方法可能会抛出TaskCanceledException和/或OperationCanceledException (两种具体类型似乎都是可能的)? 目前,文档表明HttpRequestException处理超时,这是不正确且具有误导性的。

@qidydl这就是计划......将完整记录超时可能出现的任何方式以及如何详细说明它们。

感谢@alnikola@scalablecory以及@davidsh@karelz以及参与此讨论/实施的所有其他人,我非常感谢。

此页面是否有帮助?
0 / 5 - 0 等级