C#使用using包裹HttpClient返回Task触发TaskCanceledException,如何修复?
现象描述
以下代码可以正常运行:
HttpResponseMessage responseMessage = await GetResponseMessage(pathWithParams: $"https://api.webasd.com/crm/v3565/objects/contacts?&limit=100&after={afterValue}&archived=false"); private Task<HttpResponseMessage> GetResponseMessage(string pathWithParams = null, HttpRequestMessage requestMessage = null) { var httpClient = new HttpClient(); var bearerToken = _configuration["BEARER_TOKEN"]; httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); if (pathWithParams != null) { return httpClient.GetAsync(pathWithParams); } return httpClient.SendAsync(requestMessage); }
但使用using包裹HttpClient后,会抛出TaskCanceledException异常:
HttpResponseMessage responseMessage = await GetResponseMessage(pathWithParams: $"https://api.webasd.com/crm/v3565/objects/contacts?&limit=100&after={afterValue}&archived=false"); private Task<HttpResponseMessage> GetResponseMessage(string pathWithParams = null, HttpRequestMessage requestMessage = null) { using (var httpClient = new HttpClient()) { var bearerToken = _configuration["BEARER_TOKEN"]; httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); if (pathWithParams != null) { return httpClient.GetAsync(pathWithParams); } return httpClient.SendAsync(requestMessage); } }
异常信息:
System.Threading.Tasks.TaskCanceledException: The operation was canceled. at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken) at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken) at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) at System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) at System.Net.Http.HttpClient.FinishSendAsyncBuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts) at KLV.IdentityServer.Core.Services.ContactService.GetAllContacts() in C:\Users\mk\Documents\project\sso\KLV.IdentityServer.Core\Services\ContactService.cs:line 309 at KLV.IdentityServer.Controller.ApiController.ContactsController.GetListOfContacts() in C:\Users\mk\Documents\project\sso\KLV.IdentityServer\Controller\ApiController\ContactsController.cs:line 64 at lambda_method(Closure , Object ) at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult() at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Logged|12_1(ControllerActionInvoker invoker) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker) at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at IdentityServer4.Hosting.IdentityServerMiddleware.Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events) at IdentityServer4.Hosting.MutualTlsTokenEndpointMiddleware.Invoke(HttpContext context, IAuthenticationSchemeProvider schemes) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at IdentityServer4.Hosting.BaseUrlMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
核心原因
using块会在返回Task后立即释放HttpClient,但此时异步请求尚未完成,导致底层连接被中断,最终抛出TaskCanceledException。
修复方案
方案1:await异步操作后再释放HttpClient
将方法改为async并对异步请求使用await,确保using块等待请求完成后再释放HttpClient:
HttpResponseMessage responseMessage = await GetResponseMessage(pathWithParams: $"https://api.webasd.com/crm/v3565/objects/contacts?&limit=100&after={afterValue}&archived=false"); private async Task<HttpResponseMessage> GetResponseMessage(string pathWithParams = null, HttpRequestMessage requestMessage = null) { using (var httpClient = new HttpClient()) { var bearerToken = _configuration["BEARER_TOKEN"]; httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); if (pathWithParams != null) { return await httpClient.GetAsync(pathWithParams); } return await httpClient.SendAsync(requestMessage); } }
这种方式保证了HttpClient在请求完全结束后才会被释放,避免提前中断请求。
方案2:通过依赖注入复用HttpClient(推荐)
在ASP.NET Core中,官方推荐使用IHttpClientFactory来管理HttpClient的生命周期,既不用手动处理释放逻辑,还能复用连接池提升性能:
- 在
Program.cs(.NET 6+)中注册HttpClient:
builder.Services.AddHttpClient("WebAsdApi", client => { client.BaseAddress = new Uri("https://api.webasd.com/crm/v3565/"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", builder.Configuration["BEARER_TOKEN"]); });
- 在服务中注入
IHttpClientFactory并使用:
private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; public ContactService(IHttpClientFactory httpClientFactory, IConfiguration configuration) { _httpClientFactory = httpClientFactory; _configuration = configuration; } private async Task<HttpResponseMessage> GetResponseMessage(string pathWithParams = null, HttpRequestMessage requestMessage = null) { var httpClient = _httpClientFactory.CreateClient("WebAsdApi"); if (pathWithParams != null) { return await httpClient.GetAsync(pathWithParams); } return await httpClient.SendAsync(requestMessage); }
IHttpClientFactory会自动维护HttpClient的生命周期,复用底层的HttpMessageHandler,避免频繁创建和释放导致的socket资源耗尽问题。
方案3:手动延迟释放(不推荐)
如果必须手动管理,可以在请求完成后再释放HttpClient,但这种方式代码冗余且容易出错,仅作为备选:
private async Task<HttpResponseMessage> GetResponseMessage(string pathWithParams = null, HttpRequestMessage requestMessage = null) { var httpClient = new HttpClient(); try { var bearerToken = _configuration["BEARER_TOKEN"]; httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken); HttpResponseMessage response; if (pathWithParams != null) { response = await httpClient.GetAsync(pathWithParams); } else { response = await httpClient.SendAsync(requestMessage); } return response; } finally { httpClient.Dispose(); } }
这种方式本质和方案1类似,但手动写try-finally不如using简洁,不推荐使用。
内容的提问来源于stack exchange,提问作者Anarkie

