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