2017-12-26 15 views
2

Ich verwende .NET Framework 4.6.1.HttpClient-Header wird bei der Verwendung in asynchronen Methoden null gemacht

Ich habe einen Controller in meinem Web-API, wo ich statische HttpClient haben, um alle HTTP-Anfragen zu behandeln. Nachdem ich auf IIS meine app gehostet, etwa einmal im Monat, erhalte ich die folgende Ausnahme für alle eingehenden Anfrage an meine App:

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 

Wenn ich die app-Pool auf IIS neu starten, ist alles wieder in Ordnung zu arbeiten beginnt, . Hier ist der Code, den ich habe:

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

Kann mir jemand Rat/Hilfe geben, um das Problem herauszufinden? Ich weiß nur nicht, ob das Problem in der Art ist, wie ich HttpClient mit Aufgaben verwende oder etwas Schlimmes auf IIS passiert.

+0

_ "... oder etwas Schlimmes passiert auf IIS" _ - nicht völlig unmöglich, aber _extremely_ unwahrscheinlich. Aber ohne eine gute [mcve], die das Problem zuverlässig reproduziert, gibt es nichts, was die Stack Overflow-Community als eine gute, spezifische Antwort anbieten kann. –

+0

@PeterDuniho Das Problem ist, dass ich das Problem nicht reproduzieren kann, wenn ich Anfragen manuell mache. Vielleicht kannst du mir einen Rat geben, wo ich anfangen soll, das Problem zu untersuchen? – Nomad

+2

Der erste Schritt ist der gleiche wie bei jedem anderen Problem: das Szenario so einfach wie möglich zu vereinfachen. Da Sie sagen, dass es nur einmal im Monat passiert, kann es eine Weile dauern, diese Route zu gehen. Eine weitere Standardtechnik ist das Hinzufügen von Protokollierung. In diesem Fall könnte das die beste Wette sein. Angenommen, Sie haben zumindest eine Vorstellung von dem allgemeinen Bereich des Codes, der ein Problem verursacht, fügen Sie die Protokollierung hinzu, um den gesamten Status aufzuzeichnen, damit Sie eine Vorstellung davon bekommen, welcher Status zur Reproduktion des Problems erforderlich ist. ... –

Antwort

4

Mit Blick auf die Umsetzung der DefaultRequestHeaders, können wir sehen, dass es ein einfaches Wörterbuch die Header speichern verwendet:

private Dictionary<string, HttpHeaders.HeaderStoreItemInfo> headerStore; 

DefaultRequestHeaders.Accept.Clear entfernt nur den Schlüssel aus dem Wörterbuch, ohne jede Art von Synchronisation:

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

Dictionary.Remove ist nicht threadsicher, unvorhersehbares Verhalten kann auftreten, wenn Sie während dieses Vorgangs auf das Wörterbuch zugreifen.

Nun, wenn wir uns die ParseRawHeaderValues Methode in der Stacktrace:

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

Wir können sehen, dass die Fehlerursache durch info null werden würde. Jetzt bei dem Anrufer suchen:

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

Lange Rede kurzer Sinn, iterieren wir das Wörterbuch in DefaultRequestHeaders (das ist sourceHeaders.headerStore) und die Header in der Anfrage kopieren.

Zusammenfassend haben wir gleichzeitig einen Thread, der den Inhalt des Wörterbuchs iteriert, und einen weiteren, der Elemente hinzufügt/entfernt. Dies kann zu dem Verhalten führen, das Sie sehen.

Um dies zu beheben, haben Sie zwei Möglichkeiten:

  1. initialisieren DefaultRequestHeaders in einem statischen Konstruktor, dann ändern sich nie:

    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. statt PutAsync
  3. Verwenden SendAsync mit Ihren eigenen Header:

    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)) 
    { 
        // ... 
    } 
    

Just for fun, ein kleines Repro:

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

Ausgang:

System.Net.Http.Headers.Httpheaders + HeaderStoreItemInfo

Iteration 137, Wert null

das Problem Reproducing ein paar Versuche dauern kann, da Threads in einer Endlosschleife hängen bleiben neigen, wenn das Wörterbuch gleichzeitig zugreifen (wenn es auf Ihrem Webserver geschieht, ASP.NET bricht den Thread nach Ablauf des Zeitlimits ab.

+1

@Nomad - Option 2 (und optional auch Option 1) von Kevins Antwort ist was Sie brauchen machen. Sie sollten 'DefaultRequestHeaders' nicht für jede Anfrage ändern. sie sind so gedacht, wie sie auf der Dose sagen; d. h. ** Standardwerte **, die für alle Nachrichten gelten, die diese Instanz von "HttpClient" sendet. Sie erstellen nur eine Race-Bedingung über Threads hinweg, indem Sie sie ändern. Pro-Request-Header sollten zur 'HttpRequestMessage' hinzugefügt werden, nicht zum' HttpClient'. – sellotape

+0

Vielen Dank für die ausführliche Erklärung. Ich habe meinen Code geändert und die App auf einem Server aktualisiert. Jetzt werde ich überwachen, wie es sich verhält. – Nomad

Verwandte Themen