我正在使用.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上发生了一些问题。

最佳答案

查看DefaultRequestHeaders的实现,我们可以看到它使用一个简单的Dictionary存储标头:

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方法:

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


我们可以看到该错误是info为null引起的。现在查看呼叫者:

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)并将标头复制到请求中。

总结起来,与此同时,我们有一个线程迭代字典的内容,以及另一个添加/删除元素。这可能会导致您看到的行为。

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


在静态构造函数中初始化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"));
}

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();


输出:


  System.Net.Http.Headers.HttpHeaders + HeaderStoreItemInfo
  
  迭代137,值为null


重现此问题可能需要进行一些尝试,因为并发访问字典时线程倾向于陷入无限循环(如果发生在您的Web服务器上,则ASP.NET将在超时后中止线程)。

关于c# - 在异步方法中使用时,HttpClient header 将为空,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/47973590/

10-09 20:29
查看更多