.NET HttpClient 超时:TaskCanceledException 和 TimeoutRejectedException

当 API 太慢时,.NET应用将获取 2 个异常中的 1 个,具体取决于触发的超时。 HttpClient.Timeout 抛出一个 TaskCanceledException。 来自Microsoft.Extensions.Http.Resilience的标准弹性处理程序会引发 Polly 的TimeoutRejectedException。 它们来自不同的地方,具有不同的默认值,并且需要单独的 catch 块。

触发了哪个超时

Timeout 默认 你的代码得到什么 你在哪里设置它
HttpClient.Timeout 100 秒 TaskCanceledException,以 TimeoutException 作为其 InnerException(.NET 5 及更高版本) HttpClient.Timeout
标准处理程序的单次尝试超时 每次尝试 10 秒 起初没有任何反应:处理程序会重试该操作 AddStandardResilienceHandler(options => ...)
标准处理程序总超时 30 秒,包括所有重试 Polly.Timeout.TimeoutRejectedException AddStandardResilienceHandler(options => ...)

HttpClient.Timeout

HttpClient.Timeout 适用于 HttpClient 实例发送的每个请求。 若要对某个请求使用不同的超时时间,请传递具有独立超时时间的CancellationTokenSource所提供的CancellationToken。 两者中的短者适用。 设置Timeout.InfiniteTimeSpan以将其关闭。

在 .NET 5 及更高版本上,超时会引发内部包含TimeoutException的TaskCanceledException。 在早期版本的 .NET Core 上,内部异常不存在。 在 .NET Framework 上,你会改为得到HttpRequestException。 有关详细信息,请参阅 HttpClient.Timeout 和 使用 HttpClient 类发出 HTTP 请求。

TaskCanceledException也可能表示有人取消了请求,例如关闭了页面的用户。 若要区分超时和取消,请检查ex.InnerException is TimeoutException,或检查你自己的令牌是否已取消。

标准复原处理程序

AddStandardResilienceHandler() 将速率限制器、总超时、重试、断路器和尝试超时链接在一起。 一次尝试花费的时间超过 10 秒时,尝试超时会取消它,重试策略会重试:最多 3 次重试,从 2 秒开始,指数退避和抖动。 当整个请求(包括重试)耗时超过 30 秒时,总超时会取消它,你的代码会得到TimeoutRejectedException。

TimeoutRejectedException 派生自 Exception。 它既不是一个 TimeoutException 也不是一个 HttpRequestException,所以一个 catch (HttpRequestException) 块不会捕获它。 有关默认值的完整列表,请参阅 标准复原处理程序默认值。

例如,当使用标准处理程序默认值的 .NET 10 应用调用每个响应需要 11 到 15 秒的 API 时,该应用会在 30 秒后得到TimeoutRejectedException,并且其catch (HttpRequestException)块不会运行。

如何处理 HttpClient 超时

  1. 在调用 API 的地方捕获这两个异常。 如果你使用标准处理程序,请捕获TimeoutRejectedException。 针对HttpClient.Timeout,捕获TaskCanceledException。
  2. 区分超时和取消。 仅当TaskCanceledException的内部异常是TimeoutException时才将其视为超时。 当调用方取消时,静默停止。
  3. 选择适合 API 的超时。 如果 API 经常需要超过 10 秒,请在AddStandardResilienceHandler(options => ...)中更改单次尝试超时和总超时。
  4. 不要重试POST或PATCH,除非 API 能确保重试安全。 标准处理程序默认重试所有方法,包括 POST。 调用options.Retry.DisableForUnsafeHttpMethods()以排除POST、PATCH、PUT、DELETE和CONNECT,或调用options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch)以继续重试幂等的PUT和DELETE。
  5. 告诉用户发生了什么事。 显示“服务速度缓慢,重试”而不是常规错误。
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;
    }
}

如何测试应用处理超时

你在开发时很少看到超时,因此你的catch块很少运行。 测试的方式决定你是否在用户之前找到 bug。

Approach 你找到的内容 您错过的内容
等待生产 真实超时 所有情况,直到用户遇到它
在测试中模拟 API,或让编码代理编写模拟对象 如果 mock 抛出正确的异常,你的 catch 块是否会运行 你的真实HttpClient设置、弹性处理程序的重试,以及它实际引发的异常。 你的应用还需要一个仅用于测试的开关来访问模拟。
调用真正的 API,并期望其响应较慢 实际行为 你不能故意让 API 变慢
截获应用的实际流量并延迟响应 你的真实HttpClient、弹性处理程序和异常 你的应用无需做任何更改,因此它不会对你的代码进行隔离测试。 把那个留给单元测试。

在你的应用上尝试

Dev Proxy 会截获应用对 API 的请求,并使用 LatencyPlugin 延迟响应。 .NET使用系统代理,因此无需更改代码。 此示例将每个响应延迟 11 到 15 秒,比标准处理程序的 10 秒尝试超时时间长。 将 https://api.contoso.com 替换为你的应用调用的 API 的 URL。

文件: devproxyrc.json

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

使用 devproxy --config-file devproxyrc.json 启动 Dev Proxy,然后运行应用。 每次尝试都超时,处理程序重试,30 秒后你的代码得到TimeoutRejectedException。 若要改为测试 HttpClient.Timeout,请将 minMs 设置为高于您配置的超时值。 有关设置详细信息,请参阅将开发代理用于.NET应用程序。 若要安装 Dev Proxy,请参阅“设置 Dev Proxy”。

后续步骤

另请参阅