Polly与HttpClient集成出现请求无限重试挂起问题求助
从你的代码和日志来看,请求挂起以及日志顺序异常主要由几个关键问题导致,下面逐一分析并给出修复方法:
1. 同步阻塞引发的死锁(最可能的挂起原因)
在处理响应内容时,你使用了同步阻塞的.Result:
string responseContent = response.Content.ReadAsStringAsync().Result;
在异步方法中调用.Result或.Wait()会直接阻塞当前线程,尤其是在存在同步上下文(比如ASP.NET、WinForms/WPF)的环境下,极易触发死锁——异步操作完成后需要回到原上下文,但原线程正被阻塞等待结果,最终导致整个请求卡住无法继续。
修复方法:替换为异步等待await,并配合ConfigureAwait(false)规避上下文切换风险:
string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
2. 频繁创建HttpClient导致套接字耗尽
你在每次调用callAsync时都新建HttpClient实例:
httpcli = new HttpClient();
HttpClient的设计定位是长生命周期复用对象,频繁创建会导致系统中积累大量处于TIME_WAIT状态的套接字,最终耗尽可用端口,新请求无法发起,表现为请求挂起。
修复方法:将HttpClient设置为静态成员全局复用,初始化逻辑放在静态构造函数中:
// 静态实例,全局复用 private static readonly HttpClient _httpClient = new HttpClient(); private static readonly int _maxRetryAttempts = 3; private static readonly TimeSpan _pauseBetweenFailures = TimeSpan.FromSeconds(10); static YourClassName() { // 初始化安全协议 ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; // 预设授权头 _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ConfigurationManager.AppSettings["XXXAuth"].ToString()); // 移除ConnectionClose配置,让HttpClient自动管理连接池 // _httpClient.DefaultRequestHeaders.ConnectionClose = true; }
3. 重试日志顺序异常的原因
你的日志显示重试次数是3 → 2 → 1,这是因为Polly的WaitAndRetryAsync回调中,retryCount代表即将执行的重试次数,而非已完成的次数。比如设置maxRetryAttempts = 3时:
- 第一次失败后,准备执行第1次重试,
retryCount=1 - 第二次失败后,准备执行第2次重试,
retryCount=2 - 第三次失败后,准备执行第3次重试,
retryCount=3
日志顺序错乱大概率是异步日志写入的时序问题,若要明确重试进度,可以调整日志描述:
.WaitAndRetryAsync(_maxRetryAttempts, j => _pauseBetweenFailures, (result, timeSpan, retryCount, context) => { Logger.WriteInfo($"Request failed with {result.Result?.StatusCode ?? HttpStatusCode.InternalServerError}. About to retry {retryCount}/{_maxRetryAttempts}. Waiting {timeSpan}..."); });
4. 不必要的ConnectionClose设置
你设置了httpcli.DefaultRequestHeaders.ConnectionClose = true;,这会强制每次请求后关闭TCP连接,破坏HttpClient的连接池机制,增加TCP握手开销,同时可能加剧套接字耗尽问题,建议直接移除该配置。
修复后的完整代码示例
// 静态HttpClient实例,全局复用 private static readonly HttpClient _httpClient = new HttpClient(); private static readonly int _maxRetryAttempts = 3; private static readonly TimeSpan _pauseBetweenFailures = TimeSpan.FromSeconds(10); static YourClassName() { // 初始化安全协议 ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; // 设置默认Authorization头 _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ConfigurationManager.AppSettings["XXXAuth"].ToString()); } public static async Task<ResponseWs> callAsync(int IdHeader) { ResponseWs w = new ResponseWs(); w.retCode = 1; w.Message = "Errore Generico"; getInbound(IdHeader, out int i, out string jsonObject, out string orderNum); string url = String.Format("{0}charge?orderNum={1}", ConfigurationManager.AppSettings["XXXUrl"], orderNum); Logger.WriteInfo(url); Logger.WriteInfo(jsonObject); var retryPolicy = Policy .Handle<HttpRequestException>() .OrResult<HttpResponseMessage>(r => r.StatusCode != HttpStatusCode.OK) .WaitAndRetryAsync(_maxRetryAttempts, j => _pauseBetweenFailures, (result, timeSpan, retryCount, context) => { Logger.WriteInfo($"Request failed with {result.Result?.StatusCode ?? HttpStatusCode.InternalServerError}. About to retry {retryCount}/{_maxRetryAttempts}. Waiting {timeSpan}..."); }); try { var response = await retryPolicy.ExecuteAsync(async () => { var cont = new StringContent(jsonObject, Encoding.UTF8, "application/json"); // 使用构建好的目标URL,替换硬编码的测试地址 var post = await _httpClient.PostAsync(url, cont).ConfigureAwait(false); return post; }); if (response.IsSuccessStatusCode) { string responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); XXXError sperr = JsonConvert.DeserializeObject<XXXError>(responseContent); if (sperr.status.Equals("OK", StringComparison.OrdinalIgnoreCase)) { Logger.WriteInfo("OK"); w.retCode = 0; w.Message = "OK"; return w; } else { Logger.WriteError(responseContent); Logger.WriteError(sperr.message); w.retCode = 2; w.Message = sperr.message; return w; } } else { Logger.WriteError($"Errore: {response.StatusCode}"); string errorContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false); Logger.WriteError(errorContent); w.retCode = 1; w.Message = "Errore Chiamata MW"; return w; } } catch (HttpRequestException e) { Logger.WriteError(e.Message); if (e.InnerException != null) Logger.WriteError(e.InnerException.Message); w.retCode = 1; w.Message = "Errore Chiamata MW"; return w; } catch (Exception ex) { Logger.WriteError(ex.Message); if (ex.InnerException != null) Logger.WriteError(ex.InnerException.Message); w.retCode = 1; w.Message = "Errore Chiamata MW"; return w; } }
额外注意事项
- 确保
maxRetryAttempts和pauseBetweenFailures的值合理,避免过度重试给目标服务造成压力; - 即使是非成功状态码,也建议用
await读取响应内容,避免同步阻塞; - 移除了
AggregateException的捕获,因为Polly在重试失败后,若为结果失败(如返回400)会直接返回失败响应,若为异常失败则抛出最后一次的异常,无需单独捕获AggregateException。
内容的提问来源于stack exchange,提问作者DottorCry DottorCry

