polly.timeoutasync 是最干净、可组合的超时策略,但仅作用于协作式取消,无法中断 dns 解析、tcp 连接等底层 i/o;必须配合 httpclient.timeout 使用,并置于重试外层、熔断器内层。

直接说结论:Polly.TimeoutAsync 是最干净、可组合的超时策略,但它**不能替代 HttpClient.Timeout,也不能强制中断底层 I/O**;它只在协作式取消生效的前提下起作用——也就是说,你调用的异步方法(比如 HttpClient.GetAsync)必须真正响应 CancellationToken,否则超时只是“放弃等待”,不是“停止执行”。
为什么 Polly.TimeoutAsync 有时像没生效?
常见现象是:设置了 3 秒超时,但请求卡在 DNS 解析或 TCP 连接阶段长达 20 秒才抛异常。这是因为:
-
Polly.TimeoutAsync本质是包装一层CancellationTokenSource.CancelAfter,它只对传入的Func<cancellationtoken task></cancellationtoken>生效 - 而
HttpClient.SendAsync在连接建立前(如 DNS 查询、SYN 重传)不检查 token,.NET 不暴露这些底层 socket 超时控制点 - 所以真正卡住时,Polly 只能等系统级超时(如 OS 的 connect timeout,默认常为 21 秒),它自己无能为力
换句话说:Polly 超时管的是「逻辑执行时间」,不是「网络链路时间」。
Polly.TimeoutAsync 正确用法与参数陷阱
必须配合真正支持取消的 API 使用,且注意策略类型和异常处理方式:
- 用
TimeoutStrategy.Pessimistic:这是默认值,会在超时后主动调用cancellationTokenSource.Cancel(),适合绝大多数场景 - 避免用
TimeoutStrategy.Optimistic:它只靠外部传入的 token 触发取消,不主动 Cancel,几乎没用 - 超时异常是
TimeoutRejectedException(来自 Polly),不是TimeoutException或OperationCanceledException,捕获时别写错类型 - 不要把
TimeoutAsync单独用在普通同步方法上——它要求目标委托是 async,否则会立刻完成,失去意义
示例:
var timeoutPolicy = Policy.TimeoutAsync<httpresponsemessage>(
TimeSpan.FromSeconds(5),
TimeoutStrategy.Pessimistic
);
try
{
var response = await timeoutPolicy.ExecuteAsync(
async ct => await client.GetAsync("https://api.example.com", ct),
CancellationToken.None
);
}
catch (TimeoutRejectedException)
{
// 注意:这里 catch 的是 Polly 自己的异常
Console.WriteLine("Polly 超时触发");
}
catch (HttpRequestException ex) when (ex.InnerException is OperationCanceledException)
{
// 这里才是 HttpClient 主动取消(如因 CTS.Cancel())
}</httpresponsemessage>
必须和 HttpClient.Timeout 配合使用
单靠 Polly 无法覆盖连接建立阶段,所以实际项目中要双保险:
-
HttpClient.Timeout控制整个请求生命周期上限(含连接、TLS、发送、接收),设为略大于 Polly 超时值,例如 Polly 设 5s,HttpClient.Timeout 设 8s - 二者叠加后,短时波动由 Polly 快速响应,长链路卡死由 HttpClient 底层兜底
- 注意:每次新建
HttpClient实例都会重置连接池,务必用IHttpClientFactory注册复用,否则Timeout设置可能失效 - 如果用了
AddPolicyHandler,HttpClient.Timeout仍有效,但 Polly 策略会包裹在它外层——即先走 Polly 超时逻辑,再进 HttpClient 内部超时判断
容易被忽略的熔断与重试协同问题
超时经常和重试、熔断一起配置,但顺序和语义容易出错:
- 重试策略(如
HandleTransientHttpError().WaitAndRetryAsync)应放在TimeoutAsync外层:先决定“要不要重试”,再对每一次重试尝试加超时 - 反过来说,如果把 Timeout 包在 Retry 里面,会导致每次重试都从头计时,可能总耗时远超预期
- 熔断器(
CircuitBreakerAsync)建议放在最外层,避免连续超时请求反复冲击下游 - 所有策略组合时,异常传播链变长,
InnerException嵌套多层,日志里要展开看真实根因,不能只捕获最外层异常
真正难处理的,从来不是“怎么加超时”,而是“超时之后,下游状态是否已变更?要不要幂等补偿?连接池是否堆积了半开连接?”。这些不会自动解决,得靠设计兜底。











