C#怎么使用Refit和Polly组合_C#弹性HTTP客户端教程【推荐】
Refit与Polly整合需通过IHttpClientFactory挂载策略,AddPolicyHandler不能直接用于Refit接口。指数退避必须添加随机抖动防重试风暴。通过onRetryAsync捕获重试原因并判空。超时策略需禁用HttpClient.Timeout,否则劫持Polly超时。
在实际项目里,把 Refit 和 Polly 整合起来做弹性 HTTP 客户端,看着简单,但真落地时有好几个容易踩的坑。下面就把几个最关键的环节拆开说清楚,从注册策略、指数退避抖动,到重试原因捕捉、超时冲突,每个点都直接对应线上常见的“炸裂”场景。
Refit注册时需通过IHttpClientFactory挂Polly策略:先AddRefitClient,再ConfigureHttpClient,最后AddPolicyHandler;不可直接对Refit接口调用AddPolicyHandler,否则编译错误。

Refit注册时怎么挂Polly策略
Refit 本身不直接支持策略注入,必须通过 IHttpClientFactory 中间层来实现。核心路径很简单:先用 AddRefitClient 注册 Refit 接口,接着用 AddPolicyHandler 挂上 Polly 策略,最后交给 DI 容器解析出带弹性的客户端。
常见错误是直接对 Refit 接口调用 AddPolicyHandler,这百分百编译报错——因为 Refit 的扩展方法返回的是 IServiceCollection,不是 IHttpClientBuilder,链式根本接不上。
- 正确写法(.NET 6+ Program.cs):
builder.Services .AddRefitClient
() .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://api.github.com/")) .AddPolicyHandler(GetRetryPolicy()) .AddPolicyHandler(GetTimeoutPolicy()); GetRetryPolicy()必须返回IAsyncPolicy类型,不能是泛型Policy,否则编译就过不去。- 如果同时需要熔断 + 重试,要用
PolicyWrap组合,千万不能链式调用两次AddPolicyHandler(后者会直接覆盖前者,逻辑全白写)。
为什么指数退避要加抖动(jitter)
纯指数退避(比如 1s → 2s → 4s)看着挺合理,但在高并发场景下很容易引发“重试风暴”——大量请求在同一个毫秒级时间点集体重试,下游服务瞬间被压垮。所以加随机抖动不是优化项,是生产环境的硬性要求。
举个例子,不加抖动的策略:TimeSpan.FromSeconds(Math.Pow(2, retryAttempt));加抖动后应该改成:TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)) + TimeSpan.FromMilliseconds(new Random().Next(100, 300))。
- 抖动值建议控制在 100–500ms 区间,太大把退避效果削弱了,太小又起不到错峰作用。
new Random()在高并发下容易产生重复种子,推荐用Random.Shared(.NET 6+)或者注入线程安全的IRandom。- Polly.Extensions.Http 提供了
WaitAndRetryAsync的抖动重载,但需要手动传入sleepDurationProvider,没有一键开关。
Refit + Polly 下如何捕获具体重试原因
默认情况下,Refit 抛出的异常(比如 ApiException)只是最终失败结果,中间哪次重试失败、为什么失败,完全看不到。因此必须通过 onRetryAsync 回调来记录细节。
关键点在于:回调里的 outcome.Exception 是原始异常,outcome.Result 是 HttpResponseMessage,两者都可能为 null,一定要做判空处理。
- 示例回调写法:
onRetryAsync: (outcome, span, attempt, context) =>{ var ex = outcome.Exception?.InnerException ?? outcome.Exception; var statusCode = outcome.Result?.StatusCode ?? default; Console.WriteLine($"第{attempt}次重试,等待{span.TotalSeconds:F1}s,状态码:{statusCode},异常:{ex?.GetType().Name}");} - 不要在回调里 throw 新异常,否则会中断重试流程,直接变成失败。
- 如果用了
PolicyWrap(比如重试+熔断),onRetryAsync只对重试策略生效,熔断触发时走的是onBreak回调。
超时策略和 HttpClient.Timeout 冲突吗
会冲突,而且 HttpClient.Timeout 优先级更高——它会在 Polly 的 TimeoutPolicy 触发前就抛出 TaskCanceledException,导致 Polly 超时策略压根不执行。
解决办法也很直接:必须禁用 HttpClient.Timeout,在 ConfigureHttpClient 中设为 TimeSpan.Zero 或者干脆不设置;所有超时逻辑全部交给 Polly 去控制。
- 错误写法:
.ConfigureHttpClient(c => { c.BaseAddress = ...; c.Timeout = TimeSpan.FromSeconds(10); // ❌ 这会劫持 Polly 超时 - 正确写法:
.ConfigureHttpClient(c => c.BaseAddress = ...); // 不设 Timeout
,再用AddPolicyHandler(Policy.TimeoutAsync(10))。 Policy.TimeoutAsync对HttpResponseMessage生效,而HttpClient.Timeout对整个SendAsync生命周期生效,语义完全不一样。
Refit 和 Polly 的组合看着简单,实际落地时最常卡在策略类型匹配、回调空引用、以及 HttpClient 原生超时的隐式干扰上——这些地方不报编译错、也没日志,但运行时行为完全不符合预期。


































