代码之家  ›  专栏  ›  技术社区  ›  Nomad

HttpClient标头在异步方法中使用时变为Null

  •  4
  • Nomad  · 技术社区  · 7 年前

    我正在使用。NET Framework 4.6.1。

    我的web api中有一个控制器,其中有静态HttpClient来处理所有http请求。在我将我的应用程序托管在IIS上之后,大约每月一次,我对我的应用程序的所有传入请求都会出现以下异常:

    System.ArgumentNullException: Value cannot be null.
       at System.Threading.Monitor.Enter(Object obj)
       at System.Net.Http.Headers.HttpHeaders.ParseRawHeaderValues(String name, HeaderStoreItemInfo info, Boolean removeEmptyHeader)
       at System.Net.Http.Headers.HttpHeaders.AddHeaders(HttpHeaders sourceHeaders)
       at System.Net.Http.Headers.HttpRequestHeaders.AddHeaders(HttpHeaders sourceHeaders)
       at System.Net.Http.HttpClient.PrepareRequestMessage(HttpRequestMessage request)
       at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
       at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
       at System.Net.Http.HttpClient.PutAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken)
       at Attributes.Controllers.AttributesBaseController.<UpdateAttributes>d__6.MoveNext() in D:\Git\PortalSystem\Attributes\Controllers\AttributesBaseController.cs:line 42
    

    如果我在IIS上重新启动应用程序池,一切都会恢复正常。以下是我的代码:

    public class AttributesBaseController : ApiController
    {
        [Inject]
        public IPortalsRepository PortalsRepository { get; set; }
    
        private static HttpClient Client = new HttpClient(new HttpClientHandler { Proxy = null, UseProxy = false })
                                                                                { Timeout = TimeSpan.FromSeconds(double.Parse(WebConfigurationManager.AppSettings["httpTimeout"])) };
        private static readonly Logger logger = LogManager.GetCurrentClassLogger();
    
        protected async Task UpdateAttributes(int clientId, int? updateAttrId = null)
        {
            try
            {
                Client.DefaultRequestHeaders.Accept.Clear();
                Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    
                #region Update Client Dossier !!! BELOW IS LINE 42 !!!!          
                using (var response = await Client.PutAsync(new Uri(WebConfigurationManager.AppSettings["dossier"] + "api/dossier?clientId=" + clientId), null))
                {
                    if (!response.IsSuccessStatusCode)
                    {
                        logger.Error($"Dossier update failed");
                    }
                }
                #endregion
    
                #region Gather Initial Info
                var checkSystems = PortalsRepository.GetCheckSystems(clientId);
                var currentAttributes = PortalsRepository.GetCurrentAttributes(clientId, checkSystems);
                #endregion
    
                List<Task> tasks = new List<Task>();
                #region Initialize Tasks
                foreach (var cs in checkSystems)
                {
                    if (!string.IsNullOrEmpty(cs.KeyValue))
                    {
                        tasks.Add(Task.Run(async () =>
                        {
                                var passedAttributes = currentAttributes.Where(ca => ca.SystemId == cs.SystemId && ca.AttributeId == cs.AttributeId && 
                                (ca.SysClientId == cs.KeyValue || ca.OwnerSysClientId == cs.KeyValue)).ToList();
    
                                if (cs.AttributeId == 2 && (updateAttrId == null || updateAttrId == 2))
                                {
                                    await UpdateOpenWayIndividualCardsInfo(passedAttributes, cs, clientId);
                                }
                                else if (cs.AttributeId == 3 && (updateAttrId == null || updateAttrId == 3))
                                {
                                    await UpdateEquationAccountsInfo(passedAttributes, cs, clientId);
                                }
                                else if (cs.AttributeId == 8 && (updateAttrId == null || updateAttrId == 8))
                                {
                                    await UpdateOpenWayCorporateInfo(passedAttributes, cs, clientId);
                                }
                                else if (cs.AttributeId == 9 && (updateAttrId == null || updateAttrId == 9))
                                {
                                    await UpdateEquationDealsInfo(passedAttributes, cs, clientId);
                                }
                                else if (cs.AttributeId == 10 && (updateAttrId == null || updateAttrId == 10))
                                {
                                    await UpdateOpenWayIndividualCardDepositsInfo(passedAttributes, cs, clientId);
                                }
                                else if (cs.AttributeId == 16 && (updateAttrId == null || updateAttrId == 16))
                                {
                                    await UpdateOpenWayBonusInfo(passedAttributes, cs, clientId);
                                }
                                else if (cs.AttributeId == 17 && (/*updateAttrId == null ||*/ updateAttrId == 17))
                                {
                                    await UpdateExternalCardsInfo(passedAttributes, cs, clientId);
                                }
                                if (cs.AttributeId == 18 && (updateAttrId == null || updateAttrId == 18))
                                {
                                    await UpdateCRSInfo(passedAttributes, cs, clientId);
                                }
                                else if (cs.AttributeId == 22 && (updateAttrId == null || updateAttrId == 22))
                                {
                                    await UpdateCardInsuranceInfo(passedAttributes, cs, clientId);
                                }
                        }));
                    }
                }
                #endregion
    
                // Run all tasks
                await Task.WhenAny(Task.WhenAll(tasks.ToArray()), Task.Delay(TimeSpan.FromSeconds(double.Parse(WebConfigurationManager.AppSettings["taskWaitTime"]))));
            }
            catch (Exception ex)
            {
                logger.Error(ex);
            }
        }
    }
    

    有人能给我建议/帮助我解决这个问题吗?我只是不知道是我在任务中使用HttpClient的方式有问题,还是IIS上发生了一些不好的事情。

    1 回复  |  直到 7 年前
        1
  •  8
  •   Kevin Gosse    7 年前

    查看 DefaultRequestHeaders ,我们可以看到它使用一个简单的字典来存储标题:

    private Dictionary<string, HttpHeaders.HeaderStoreItemInfo> headerStore;
    

    DefaultRequestHeaders.Accept.Clear 只需从字典中删除密钥,无需任何同步:

    public bool Remove(string name)
    {
      this.CheckHeaderName(name);
      if (this.headerStore == null)
        return false;
      return this.headerStore.Remove(name);
    }
    

    Dictionary.Remove 不是线程安全的,如果在此操作期间访问字典,可能会发生不可预测的行为。

    现在如果我们看看 ParseRawHeaderValues stacktrace中的方法:

    private bool ParseRawHeaderValues(string name, HttpHeaders.HeaderStoreItemInfo info, bool removeEmptyHeader)
    {
      lock (info)
      {
        // stuff
      }
      return true;
    }
    

    我们可以看出错误的原因是 info 为空。现在看看来电者:

    internal virtual void AddHeaders(HttpHeaders sourceHeaders)
    {
      if (sourceHeaders.headerStore == null)
        return;
      List<string> stringList = (List<string>) null;
      foreach (KeyValuePair<string, HttpHeaders.HeaderStoreItemInfo> keyValuePair in sourceHeaders.headerStore)
      {
        if (this.headerStore == null || !this.headerStore.ContainsKey(keyValuePair.Key))
        {
          HttpHeaders.HeaderStoreItemInfo headerStoreItemInfo = keyValuePair.Value;
          if (!sourceHeaders.ParseRawHeaderValues(keyValuePair.Key, headerStoreItemInfo, false))
          {
            if (stringList == null)
              stringList = new List<string>();
            stringList.Add(keyValuePair.Key);
          }
          else
            this.AddHeaderInfo(keyValuePair.Key, headerStoreItemInfo);
        }
      }
      if (stringList == null)
        return;
      foreach (string key in stringList)
        sourceHeaders.headerStore.Remove(key);
    }
    

    长话短说,我们在 DefaultRequestHeaders (那是 sourceHeaders.headerStore )并将标题复制到请求中。

    综上所述,同时我们有一个线程迭代字典的内容,还有一个线程添加/删除元素。这会导致你看到的行为。

    要解决此问题,有两种解决方案:

    1. 初始化 DefaultRequestHeaders 在静态构造函数中,则永远不要更改它:

      static AttributesBaseController 
      {
          Client = new HttpClient(new HttpClientHandler { Proxy = null, UseProxy = false })
          {
              Timeout = TimeSpan.FromSeconds(double.Parse(WebConfigurationManager.AppSettings["httpTimeout"]))
          };
      
          Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
      }
      
    2. 使用 SendAsync 使用自定义标题,而不是 PutAsync :

      var message = new HttpRequestMessage(HttpMethod.Put, new Uri(WebConfigurationManager.AppSettings["dossier"] + "api/dossier?clientId=" + clientId));
      message.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
      using (var response = await Client.SendAsync(message))
      {
           // ...
      }
      

    只是为了好玩,一个小小的复制品:

    var client = new HttpClient();
    
    client.DefaultRequestHeaders.Accept.Clear();
    client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    
    var storeField = typeof(HttpHeaders).GetField("headerStore", BindingFlags.Instance | BindingFlags.NonPublic);
    
    FieldInfo valueField = null;
    
    var store = (IEnumerable)storeField.GetValue(client.DefaultRequestHeaders);
    
    foreach (var item in store)
    {
        valueField = item.GetType().GetField("value", BindingFlags.Instance | BindingFlags.NonPublic);
    
        Console.WriteLine(valueField.GetValue(item));
    }
    
    for (int i = 0; i < 8; i++)
    {
        Task.Run(() =>
        {
            int iteration = 0;
    
            while (true)
            {
                iteration++;
    
                try
                {
                    foreach (var item in store)
                    {
                        var value = valueField.GetValue(item);
    
                        if (value == null)
                        {
                            Console.WriteLine("Iteration {0}, value is null", iteration);
                        }
    
                        break;
                    }
    
                    client.DefaultRequestHeaders.Accept.Clear();
                    client.DefaultRequestHeaders.Accept.Add(new Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
                }
                catch (Exception) { }
            }
        });
    }
    
    Console.ReadLine();
    

    输出:

    系统网Http。标题。HttpHeaders+HeadersStoreItemInfo

    迭代137,值为空

    复制此问题可能需要几次尝试,因为线程在并发访问字典时往往陷入无限循环(如果发生在Web服务器上,ASP.NET将在超时后中止线程)。