You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

HttpClient超时触发TaskCanceledException时Polly重试策略失效问题

问题描述

尝试使用Microsoft.Extensions.Http.Polly策略处理器,在出现TaskCanceledException或HTTP瞬时错误时进行重试,但该策略未正常工作。

编写的最小测试API代码如下:

using Polly;
using Polly.Extensions.Http;
using Polly.Retry;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

AsyncRetryPolicy<HttpResponseMessage>? policy = HttpPolicyExtensions.HandleTransientHttpError()
    .Or<TaskCanceledException>()
    .WaitAndRetryAsync(5, _ => TimeSpan.FromSeconds(5));

builder.Services.AddHttpClient("test", client =>
    {
        client.BaseAddress = new Uri("http://localhost:5000");
        client.Timeout = TimeSpan.FromSeconds(2);
    })
    .AddPolicyHandler(policy);

WebApplication app = builder.Build();

app.MapPost("/status/{code}",
    async (string code, IHttpClientFactory httpClientFactory, CancellationToken cancellationToken) =>
    {
        HttpClient client = httpClientFactory.CreateClient("test");
        HttpResponseMessage response = await client.GetAsync($"/status/{code}", cancellationToken);

        response.EnsureSuccessStatusCode();

        return response.Content.ReadAsStringAsync(cancellationToken);
    });

app.MapPost("/timeout/{timeout:int}",
    async (int timeout, IHttpClientFactory httpClientFactory, CancellationToken cancellationToken) =>
    {
        HttpClient client = httpClientFactory.CreateClient("test");
        HttpResponseMessage response = await client.GetAsync($"/timeout/{timeout}", cancellationToken);

        response.EnsureSuccessStatusCode();

        return response.Content.ReadAsStringAsync(cancellationToken);
    });

#region Simulated API

app.MapGet("/status/{code:int}", (int code) => Results.StatusCode(code));

app.MapGet("/timeout/{timeout:int}", async (int timeout) =>
{
    await Task.Delay(TimeSpan.FromSeconds(timeout));
    return Results.Ok();
});

#endregion

app.Run();

调用POST http://localhost:5000/timeout/10后,直接抛出以下异常且未进行任何重试:

Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 2 seconds elapsing.
---&gt; System.TimeoutException: A task was canceled.
---&gt; System.Threading.Tasks.TaskCanceledException: A task was canceled.
 at Polly.Retry.AsyncRetryEngine.ImplementationAsync[TResult](Func`3 action, Context context, CancellationToken cancellationToken, ExceptionPredicates shouldRetryExceptionPredicates, ResultPredicates`1 shouldRetryResultPredicates, Func`5 onRetryAsync, Int32 permittedRetryCount, IEnumerable`1 sleepDurationsEnumerable, Func`4 sleepDurationProvider, Boolean continueOnCapturedContext)
 at Polly.AsyncPolicy`1.ExecuteAsync(Func`3 action, Context context, CancellationToken cancellationToken, Boolean continueOnCapturedContext)       
 at Microsoft.Extensions.Http.PolicyHttpMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
 at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.&lt;&gt;c__DisplayClass5_0.&lt;&lt;SendCoreAsync&gt;g__Core|0&gt;d.MoveNext()
--- End of stack trace from previous location ---
 at System.Net.Http.HttpClient.&lt;SendAsync&gt;g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
 --- End of inner exception stack trace ---
 --- End of inner exception stack trace ---
 at System.Net.Http.HttpClient.HandleFailure(Exception e, Boolean telemetryStarted, HttpResponseMessage response, CancellationTokenSource cts, CancellationToken cancellationToken, CancellationTokenSource pendingRequestsCts)
 at System.Net.Http.HttpClient.&lt;SendAsync&gt;g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
 at Program.&lt;&gt;c.&lt;&lt;&lt;Main&gt;$&gt;b__0_3&gt;d.MoveNext() in Z:\Repositories\POCs\PollyRetryTest\PollyRetryTest\Program.cs:line 35
--- End of stack trace from previous location ---
 at Microsoft.AspNetCore.Http.RequestDelegateFactory.&lt;ExecuteTaskOfT&gt;g__ExecuteAwaited|129_0[T](Task`1 task, HttpContext httpContext, JsonTypeInfo`1 jsonTypeInfo)
 at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)     

问题根源

你设置的HttpClient.Timeout是在Polly策略处理器的外层生效的。当请求超时触发TaskCanceledException时,这个异常是在Polly重试策略执行完成之后才被抛出的,导致重试策略根本没机会捕获并处理这个异常。

解决方案

要让超时异常能被重试策略捕获,需要移除HttpClient的内置超时配置,改用Polly的TimeoutPolicy来控制单请求超时,再将重试策略与超时策略组合使用。

修改后的代码示例

using Polly;
using Polly.Extensions.Http;
using Polly.Retry;
using Polly.Timeout;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// 定义重试策略:捕获瞬时HTTP错误和超时导致的TaskCanceledException
AsyncRetryPolicy<HttpResponseMessage> retryPolicy = HttpPolicyExtensions.HandleTransientHttpError()
    .Or<TaskCanceledException>(ex => ex.InnerException is TimeoutException) // 仅处理超时导致的取消异常
    .WaitAndRetryAsync(5, _ => TimeSpan.FromSeconds(5));

// 定义单请求超时策略
AsyncTimeoutPolicy<HttpResponseMessage> timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(TimeSpan.FromSeconds(2));

// 组合策略:重试包裹超时,确保每次重试都应用超时控制
var combinedPolicy = Policy.WrapAsync(retryPolicy, timeoutPolicy);

builder.Services.AddHttpClient("test", client =>
{
    client.BaseAddress = new Uri("http://localhost:5000");
    // 移除HttpClient的Timeout配置,改用Polly超时策略
})
.AddPolicyHandler(combinedPolicy);

WebApplication app = builder.Build();

app.MapPost("/status/{code}",
    async (string code, IHttpClientFactory httpClientFactory, CancellationToken cancellationToken) =>
    {
        HttpClient client = httpClientFactory.CreateClient("test");
        HttpResponseMessage response = await client.GetAsync($"/status/{code}", cancellationToken);

        response.EnsureSuccessStatusCode();

        return response.Content.ReadAsStringAsync(cancellationToken);
    });

app.MapPost("/timeout/{timeout:int}",
    async (int timeout, IHttpClientFactory httpClientFactory, CancellationToken cancellationToken) =>
    {
        HttpClient client = httpClientFactory.CreateClient("test");
        HttpResponseMessage response = await client.GetAsync($"/timeout/{timeout}", cancellationToken);

        response.EnsureSuccessStatusCode();

        return response.Content.ReadAsStringAsync(cancellationToken);
    });

#region Simulated API

app.MapGet("/status/{code:int}", (int code) => Results.StatusCode(code));

app.MapGet("/timeout/{timeout:int}", async (int timeout) =>
{
    await Task.Delay(TimeSpan.FromSeconds(timeout));
    return Results.Ok();
});

#endregion

app.Run();

关键说明

  • 策略顺序:用Policy.WrapAsync组合时,重试策略在外层,超时策略在内层。这样每次重试都会启动一个新的请求,且每个请求都受超时策略限制。
  • 异常过滤:通过Or<TaskCanceledException>(ex => ex.InnerException is TimeoutException)确保只重试超时导致的取消异常,避免重试用户主动取消请求的情况。
  • 移除HttpClient.Timeout:如果保留这个配置,它会成为所有请求的全局超时,包括整个重试流程的总时长,会提前终止重试逻辑。

内容的提问来源于stack exchange,提问作者Sergio Jr

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 20:27:03