【问题标题】:HttpClient header getting nulled when using in async methods在异步方法中使用时,HttpClient 标头为空
【发布时间】:2017-12-26 03:20:28
【问题描述】:

我正在使用 .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 上发生了一些不好的事情。

【问题讨论】:

  • "...或者 IIS 上发生了一些不好的事情" -- 并非完全不可能,但不太可能。但是,如果没有一个好的minimal reproducible example 可以可靠地重现问题,Stack Overflow 社区就无法提供任何好的、具体的答案。
  • @PeterDuniho 问题是如果我手动执行请求,我将无法重现该问题。也许你可以给我一些建议,从哪里开始调查这个问题?
  • 第一步与任何其他问题相同:尽可能简化场景。既然你说它每月只发生一次,那么走这条路可能需要一段时间。另一种标准技术是添加日志记录;在这种情况下,这可能是您最好的选择。假设您至少对导致问题的一般代码区域有所了解,请添加日志记录以记录所有状态,以便您了解重现问题所需的状态。 ...
  • ... 最初这可能只是变量和类成员,但您可能还想记录客户端数量、身份、线程池状态等内容。
  • @PeterDuniho 另外,我忘了提到我为应用程序池提供了 3 个工作进程。当这个异常发生时,所有的工作进程不会一下子失败,它们会一个一个地失败。

标签: c# .net iis async-await


【解决方案1】:

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

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

输出:

System.Net.Http.Headers.HttpHeaders+HeaderStoreItemInfo

第 137 次迭代,值为空

重现该问题可能需要多次尝试,因为线程在同时访问字典时往往会陷入无限循环(如果它发生在您的网络服务器上,ASP.NET 将在超时后中止线程)。

【讨论】:

  • @Nomad - 凯文回答的选项 2(以及可选的选项 1)是您需要做的。您不应该为每个请求更改DefaultRequestHeaders;正如他们在锡上所说的那样;即应用于HttpClient 实例发送的所有消息的默认值。您只是通过更改线程来创建跨线程的竞争条件。每个请求的标头应该添加到HttpRequestMessage,而不是HttpClient
  • 感谢您的详细解释。我更改了代码并更新了服务器上的应用程序。现在,我将监控它的行为。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-11-09
  • 1970-01-01
  • 2020-12-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-13
相关资源
最近更新 更多