C#批量调用HttpClient时Task.Wait()触发空引用异常求解
问题现象
批量遍历1000个元素的列表,逐元素调用接口获取属性时抛出空引用异常,异常堆栈如下:
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
Exception: "Object reference not set to an instance of an object."
问题关联的原始业务代码如下:
foreach(var item from list) { var result= getattributes(item.id); } private attributes getattributes(string id) { using (var client1 = new HttpClient()) { client1.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", oneTimeAuth); try { string url = connectionDetails.BaseUrl; var deviceattribute = client1.GetStringAsync(url); deviceattribute.Wait(); var deviceattributeResult = JsonConvert.DeserializeObject<deviceattributes>(deviceattribute.Result); if(deviceattributeResult != null) { return deviceattributeResult; } } catch (System.Exception ex) { WriteLog((int)Core.Enums.JobLogType.Error, $"Exception while getting I. {ex.StackTrace}"); } } return null; }
排查思路
按优先级从高到低排查以下问题点:
- 日志信息缺失:当前catch块仅打印异常堆栈,未记录异常消息、内部异常信息,且同步调用
.Wait()会把异步任务内部的真实异常包装为AggregateException,现有堆栈无法定位真实的空引用触发位置。 - 高频创建HttpClient实例:1000次循环每次都新建、释放HttpClient,会快速耗尽本机可用Socket端口,导致请求失败触发异常。
- 空值未做校验:
connectionDetails、oneTimeAuth、接口返回的响应字符串都可能为null,直接调用属性/传参就会触发空引用。 - 逻辑bug:方法传入的
id参数完全没有参与url拼接,所有请求都固定请求connectionDetails.BaseUrl,如果BaseUrl是相对路径、空值,会直接导致HttpClient请求构造失败。 - 同步阻塞异步代码:使用
.Wait()、.Result同步等待异步任务,除了异常包装问题,还很容易触发同步上下文死锁,导致任务无法正常完成。
解决方法
按以下步骤修复:
- 补全日志内容,catch块需要同时记录异常消息、所有内部异常的内容,方便定位真实报错点。
- 将HttpClient定义为全局静态单例复用,避免频繁创建销毁导致的Socket资源耗尽问题。
- 改用async/await全异步写法替换同步阻塞调用,避免异常包装和死锁问题。
- 对所有可能为null的对象提前做判空校验,url拼接时正确传入item.id参数。
- 对HTTP请求增加状态码校验,非成功状态码直接记录响应内容,方便排查接口侧问题。
修复后的参考代码:
// 全局静态复用HttpClient private static readonly HttpClient _httpClient = new HttpClient(); // 遍历逻辑改为异步 public async Task ProcessListAsync(List<Item> list) { foreach(var item in list) { var result = await GetAttributesAsync(item.id); // 后续业务逻辑 } } private async Task<attributes> GetAttributesAsync(string id) { // 前置参数判空 if (connectionDetails == null || string.IsNullOrEmpty(connectionDetails.BaseUrl)) { WriteLog((int)Core.Enums.JobLogType.Error, "connectionDetails配置为空或BaseUrl未配置"); return null; } if (string.IsNullOrEmpty(oneTimeAuth)) { WriteLog((int)Core.Enums.JobLogType.Error, "鉴权token为空"); return null; } if (string.IsNullOrEmpty(id)) { WriteLog((int)Core.Enums.JobLogType.Error, "传入的id参数为空"); return null; } try { // 按实际接口规则拼接id参数,示例为query参数格式,根据实际接口调整 var url = $"{connectionDetails.BaseUrl.TrimEnd('/')}/?id={id}"; _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", oneTimeAuth); var response = await _httpClient.GetAsync(url); // 校验HTTP请求状态 response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(); if (string.IsNullOrEmpty(responseContent)) { WriteLog((int)Core.Enums.JobLogType.Error, $"id={id} 接口返回内容为空"); return null; } var deviceattributeResult = JsonConvert.DeserializeObject<deviceattributes>(responseContent); return deviceattributeResult; } catch (Exception ex) { // 补全异常信息日志 var errorMsg = $"获取id={id}属性异常,消息:{ex.Message},内部异常:{ex.InnerException?.Message},堆栈:{ex.StackTrace}"; WriteLog((int)Core.Enums.JobLogType.Error, errorMsg); return null; } }
补充说明:如果1000个元素串行调用接口耗时太长,可以考虑用Task.WhenAll加并发控制做批量请求,但要注意接口侧的限流阈值,不要触发接口拦截。
内容的提问来源于stack exchange,提问作者user
相关产品推荐
相关产品推荐

