为何Polly重试策略仍抛出HttpClient.Timeout异常且不重试?
HttpClient超时异常无法触发Polly重试的问题排查与解决
问题描述
配置了Polly重试策略处理HttpClient的PostAsync请求,但遇到HttpClient.Timeout异常时,策略不执行重试直接抛出异常,寻求原因及解决办法。
HttpClient配置
_httpClientHandler = new HttpClientHandler() { UseCookies = true, CookieContainer = new CookieContainer(), AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate }; _httpClient = new HttpClient(_httpClientHandler) { Timeout = TimeSpan.FromSeconds(10) };
POST请求及Polly策略代码
private async Task PreparePayloadByDate(DateTime date) { // Define the retry policy with exponential backoff var retryPolicy = Policy<HttpResponseMessage>.Handle<HttpRequestException>() .Or<TaskCanceledException>() .OrResult(r => !r.IsSuccessStatusCode) .WaitAndRetryForeverAsync( retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), onRetry: (exception, retryCount, timeSpan) => { _logger.Log(LogEnums.Error, $"Attempt {retryCount} failed. Retrying in {timeSpan} seconds..."); }); var stringPayload = JsonConvert.SerializeObject(new { key = "days", options = date.ToString("yyyy-MM-dd") }); var httpContent = new StringContent(stringPayload, Encoding.UTF8, "application/json"); var r1 = await retryPolicy.ExecuteAsync(() => _httpClient.PostAsync($"{_moduleConfig.Endpoint}/options/selected", httpContent)); }
补充信息
- 曾尝试用Polly的Timeout策略替代HttpClient内置超时,问题依旧。
- 重试策略已包含在try-catch块中,抛出的异常信息为:
The request was canceled due to the configured HttpClient.Timeout of 15 seconds elapsing.,堆栈跟踪指向at System.Net.Http.HttpClient.HandleFailure(~)。 - 即使将HttpClient超时设置为3分钟、10分钟,请求仍持续超时,怀疑目标URL存在限流,但浏览器中无法复现该问题。
原因分析
- HttpContent复用失效:代码中复用了同一个
StringContent对象,而HttpContent在首次请求发送后会被自动释放,后续重试时该对象已处于处置状态,直接抛出异常导致重试逻辑中断。 - 异常捕获范围不精准:.NET 5+版本中,HttpClient超时抛出的
TaskCanceledException通常会包含TimeoutException作为内部异常,当前策略仅泛泛捕获TaskCanceledException,可能因异常包装逻辑导致识别失效。 - Polly策略组合错误:若使用Polly Timeout策略,未通过
PolicyWrap正确组合重试与超时策略,会导致超时异常无法被重试策略捕获。
解决办法
1. 修复HttpContent复用问题
每次重试时重新创建StringContent对象,避免复用已处置的内容:
var r1 = await retryPolicy.ExecuteAsync(async () => { var stringPayload = JsonConvert.SerializeObject(new { key = "days", options = date.ToString("yyyy-MM-dd") }); var httpContent = new StringContent(stringPayload, Encoding.UTF8, "application/json"); return await _httpClient.PostAsync($"{_moduleConfig.Endpoint}/options/selected", httpContent); });
2. 精准捕获超时相关异常
调整Polly策略,明确捕获由超时引发的TaskCanceledException:
var retryPolicy = Policy<HttpResponseMessage> .Handle<HttpRequestException>() .Or<TaskCanceledException>(ex => ex.InnerException is TimeoutException) .OrResult(r => !r.IsSuccessStatusCode) .WaitAndRetryForeverAsync( retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), onRetry: (result, retryCount, timeSpan) => { var errorMsg = result.Exception != null ? $"exception: {result.Exception.Message}" : $"status code: {result.Result.StatusCode}"; _logger.Log(LogEnums.Error, $"Attempt {retryCount} failed due to {errorMsg}. Retrying in {timeSpan} seconds..."); });
3. 正确组合Polly超时与重试策略
若使用Polly Timeout,需通过PolicyWrap为每个重试请求配置独立超时:
// 单请求超时策略 var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(10)); // 重试策略 var retryPolicy = Policy<HttpResponseMessage> .Handle<HttpRequestException>() .Or<TaskCanceledException>(ex => ex.InnerException is TimeoutException) .OrResult(r => !r.IsSuccessStatusCode) .WaitAndRetryForeverAsync( retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), onRetry: (result, retryCount, timeSpan) => { // 日志逻辑 }); // 组合策略:先执行超时控制,再触发重试 var policyWrap = Policy.WrapAsync(retryPolicy, timeoutPolicy); // 执行请求 var r1 = await policyWrap.ExecuteAsync(async () => { var stringPayload = JsonConvert.SerializeObject(new { key = "days", options = date.ToString("yyyy-MM-dd") }); var httpContent = new StringContent(stringPayload, Encoding.UTF8, "application/json"); return await _httpClient.PostAsync($"{_moduleConfig.Endpoint}/options/selected", httpContent); });
4. 排查目标服务限流差异
对比浏览器与HttpClient的请求头(如User-Agent、Cookie、Referer等),确保请求参数一致;若怀疑限流,可在重试逻辑中读取响应头的Retry-After字段,调整重试间隔以符合服务要求。
内容的提问来源于stack exchange,提问作者Jamie Head
相关产品推荐
相关产品推荐

