Xamarin+Refit短时间大量请求后持续出现任务取消错误
嘿,看你的问题描述和代码,这个场景在iOS使用Mono环境下的单例HttpClient时真的挺常见的,结合现象和堆栈信息,我来帮你拆解问题根源和对应的解决思路:
问题根源分析
从你给出的错误堆栈和“触发一次错误后所有请求都失效,必须重启”的现象来看,大概率是MonoWebRequestHandler底层的连接池被异常状态污染,或者并发请求时修改共享对象导致的线程安全问题。
你代码里有个很容易被忽略的风险:在SendAsync方法中直接修改了传入的HttpRequestMessage对象的Authorization Header。因为你用的是单例HttpClient,当多个请求并发进来时,这个请求对象可能会被复用或者在多个线程间共享,直接修改它会导致请求之间的令牌互相覆盖,甚至触发底层连接池的状态异常,最终导致整个Handler彻底失效。
另外,iOS的Mono HttpClient实现本身在处理短时间高并发请求时,连接池的容错性比较差,一旦出现异常就容易锁死整个连接池。
具体解决方案
1. 不要修改原始请求对象,创建副本处理
核心思路是避免并发请求之间互相干扰,每次处理请求时创建一个完整的副本,再修改副本的Header:
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Debug.WriteLine("Url: " + request.RequestUri); var auth = request.Headers.Authorization; // 关键:创建请求的深层副本,避免修改原始请求引发并发问题 var requestCopy = await CloneRequestAsync(request, cancellationToken); if (auth != null) { string token = await _reauthService.GetToken(); requestCopy.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, token); } try { HttpResponseMessage resp = await base.SendAsync(requestCopy, cancellationToken).ConfigureAwait(false); return resp; } catch (Exception e) { Crashes.TrackError(e); throw new RequestFailedException(e); } } // 辅助方法:完整复制HttpRequestMessage,包括内容和Header private async Task<HttpRequestMessage> CloneRequestAsync(HttpRequestMessage original, CancellationToken cancellationToken) { var clone = new HttpRequestMessage(original.Method, original.RequestUri) { Version = original.Version }; // 复制所有请求Header foreach (var header in original.Headers) { clone.Headers.TryAddWithoutValidation(header.Key, header.Value); } // 复制内容和内容Header if (original.Content != null) { var contentStream = new MemoryStream(); await original.Content.CopyToAsync(contentStream, cancellationToken).ConfigureAwait(false); contentStream.Position = 0; clone.Content = new StreamContent(contentStream); foreach (var header in original.Content.Headers) { clone.Content.Headers.TryAddWithoutValidation(header.Key, header.Value); } } return clone; }
2. 手动配置连接池参数,优化Mono的连接管理
针对iOS的Mono实现,我们可以手动调整连接池的参数,避免短时间大量请求耗尽连接,同时设置连接自动释放的超时:
container.RegisterSingleton(() => { var handler = container.Resolve<AuthenticatedHttpClientHandler>(); // 配置连接池参数,根据你的并发量调整 handler.MaxConnectionsPerServer = 20; // 设置连接租约超时,避免连接长时间占用 handler.ServicePointManager.ConnectionLeaseTimeout = 5000; // 5秒 handler.ServicePointManager.MaxServicePointIdleTime = 5000; HttpClient client = new HttpClient(handler) { BaseAddress = new Uri(UrlConfig.MobileApiBaseUrl + UrlConfig.Version), Timeout = TimeSpan.FromSeconds(30) }; var service = RestService.For<IMobileApiClient>(client); return service; });
3. 区分处理取消异常,避免错误扩散
"A task was canceled"不一定都是超时,有时候是底层连接被异常终止,我们可以在catch块中针对性处理,同时尝试重置Handler状态(如果是短暂生命周期的Handler的话):
catch (OperationCanceledException ex) { // 如果不是用户主动取消的,说明是底层连接/连接池问题 if (!cancellationToken.IsCancellationRequested) { Crashes.TrackError(new Exception("Connection pool encountered unexpected cancellation", ex)); // 如果Handler不是单例,可以在这里Dispose后重新创建 // 但如果是单例,这里不能直接Dispose,建议改用HttpClientFactory管理生命周期 } throw new RequestFailedException(ex); } catch (Exception e) { Crashes.TrackError(e); throw new RequestFailedException(e); }
4. 改用HttpClientFactory管理HttpClient生命周期
如果你的项目基于.NET Standard 2.1及以上,强烈建议使用IHttpClientFactory替代手动创建单例HttpClient。它会自动管理Handler的创建和回收,避免单例Handler带来的连接池问题:
// 注册服务 container.RegisterSingleton<IHttpClientFactory>(() => { var services = new ServiceCollection(); services.AddHttpClient<IMobileApiClient>(client => { client.BaseAddress = new Uri(UrlConfig.MobileApiBaseUrl + UrlConfig.Version); client.Timeout = TimeSpan.FromSeconds(30); }) .AddHttpMessageHandler<AuthenticatedHttpClientHandler>(); return services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>(); }); // 使用时从容器获取Factory,创建客户端 var apiClient = httpClientFactory.CreateClient<IMobileApiClient>();
额外建议
- 调整iOS的HttpClient实现:在项目的iOS Build设置里,把
HttpClient Implementation改成NSUrlSession,这个基于原生iOS网络栈的实现比Mono的Managed版本更稳定,对并发请求的处理更可靠。 - 检查
_reauthService.GetToken()的实现:确保这个方法是真正异步的,没有内部同步阻塞操作,否则会导致请求队列堆积,加剧连接池压力。
内容的提问来源于stack exchange,提问作者MichaelStoddart

