Web Api + HttpClient: un module ou un gestionnaire asynchrone terminé lorsqu’une opération asynchrone est en attente

J’écris une application qui dépose des requêtes HTTP à l’aide de l’API Web ASP.NET et j’ai du mal à identifier la source d’une erreur intermittente. Cela semble être une condition de course … mais je ne suis pas tout à fait sûr.

Avant d’entrer dans les détails, voici le stream de communication général de l’application:

  • Le client envoie une requête HTTP à Proxy 1 .
  • Proxy 1 transmet le contenu de la requête HTTP à Proxy 2
  • Le proxy 2 transmet le contenu de la requête HTTP à l’application Web cible
  • Target Web App répond à la requête HTTP et la réponse est diffusée (transfert en bloc) vers Proxy 2
  • Le proxy 2 renvoie la réponse au proxy 1, qui à son tour répond au client appelant d’origine.

Les applications proxy sont écrites en ASP.NET Web API RTM à l’aide de .NET 4.5. Le code pour effectuer le relais ressemble à ceci:

//Controller entry point. public HttpResponseMessage Post() { using (var client = new HttpClient()) { var request = BuildRelayHttpRequest(this.Request); //HttpCompletionOption.ResponseHeadersRead - so that I can start streaming the response as soon //As it begins to filter in. var relayResult = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).Result; var returnMessage = BuildResponse(relayResult); return returnMessage; } } private static HttpRequestMessage BuildRelayHttpRequest(HttpRequestMessage incomingRequest) { var requestUri = BuildRequestUri(); var relayRequest = new HttpRequestMessage(incomingRequest.Method, requestUri); if (incomingRequest.Method != HttpMethod.Get && incomingRequest.Content != null) { relayRequest.Content = incomingRequest.Content; } //Copies all safe HTTP headers (mainly content) to the relay request CopyHeaders(relayRequest, incomingRequest); return relayRequest; } private static HttpRequestMessage BuildResponse(HttpResponseMessage responseMessage) { var returnMessage = Request.CreateResponse(responseMessage.StatusCode); returnMessage.ReasonPhrase = responseMessage.ReasonPhrase; returnMessage.Content = CopyContentStream(responseMessage); //Copies all safe HTTP headers (mainly content) to the response CopyHeaders(returnMessage, responseMessage); } private static PushStreamContent CopyContentStream(HttpResponseMessage sourceContent) { var content = new PushStreamContent(async (stream, context, transport) => await sourceContent.Content.ReadAsStreamAsync() .ContinueWith(t1 => t1.Result.CopyToAsync(stream) .ContinueWith(t2 => stream.Dispose()))); return content; } 

L’erreur qui se produit par intermittence est la suivante:

Un module ou un gestionnaire asynchrone s’est terminé pendant qu’une opération asynchrone était toujours en attente.

Cette erreur se produit généralement lors des premières requêtes adressées aux applications proxy, après quoi l’erreur n’est plus visible.

Visual Studio n’attrape jamais l’exception lorsque lancée. Mais l’erreur peut être détectée dans l’événement Global.asax Application_Error. Malheureusement, l’exception n’a pas de trace de stack.

Les applications proxy sont hébergées dans des rôles Web Azure.

Toute aide permettant d’identifier le coupable serait appréciée.

Votre problème est subtil: le lambda async que vous transmettez à PushStreamContent est interprété comme un async void (car le constructeur PushStreamContent prend uniquement Action s en tant que parameters). Il y a donc une condition de concurrence entre l’achèvement de votre module / gestionnaire et l’achèvement de ce lambda async void .

PostStreamContent détecte la fermeture du stream et le considère comme la fin de sa Task (complétant le module / gestionnaire), il suffit donc de vérifier qu’aucune méthode async void ne peut encore être exécutée après la fermeture du stream. async Task méthodes de async Task sont correctes, donc cela devrait résoudre le problème:

 private static PushStreamContent CopyContentStream(HttpResponseMessage sourceContent) { Func copyStreamAsync = async stream => { using (stream) using (var sourceStream = await sourceContent.Content.ReadAsStreamAsync()) { await sourceStream.CopyToAsync(stream); } }; var content = new PushStreamContent(stream => { var _ = copyStreamAsync(stream); }); return content; } 

Si vous souhaitez que vos proxy évoluent un peu mieux, je vous recommande également de supprimer tous les appels de Result :

 //Controller entry point. public async Task PostAsync() { using (var client = new HttpClient()) { var request = BuildRelayHttpRequest(this.Request); //HttpCompletionOption.ResponseHeadersRead - so that I can start streaming the response as soon //As it begins to filter in. var relayResult = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); var returnMessage = BuildResponse(relayResult); return returnMessage; } } 

Votre ancien code bloquerait un thread pour chaque requête (jusqu’à la réception des en-têtes); En utilisant async jusqu’au niveau de votre contrôleur, vous ne bloquerez pas un thread pendant ce temps.

Un modèle un peu plus simple est que vous pouvez utiliser directement les HttpContents et les faire circuler dans le relais. Je viens de télécharger un exemple illustrant comment vous pouvez compter les demandes et les réponses de manière asynchrone et sans mettre en mémoire tampon le contenu de manière relativement simple:

http://aspnet.codeplex.com/SourceControl/changeset/view/7ce67a547fd0#Samples/WebApi/RelaySample/ReadMe.txt

Il est également utile de réutiliser la même instance HttpClient, car cela vous permet de réutiliser les connexions, le cas échéant.

Je voudrais append un peu de sagesse à quiconque a atterri ici avec la même erreur, mais tout votre code semble bien. Recherchez toutes les expressions lambda passées dans des fonctions dans l’arborescence des appels à partir de là où cela se produit.

Je recevais cette erreur sur un appel JavaScript JSON à une action du contrôleur MVC 5.x. Tout ce que je faisais de haut en bas de la stack était défini comme étant async Task et appelé avec await .

Cependant, à l’aide de la fonctionnalité “Définir l’instruction suivante” de Visual Studio, j’ai systématiquement ignoré les lignes pour en déterminer la cause. J’ai continué à explorer les méthodes locales jusqu’à ce que je reçoive un appel dans un package externe NuGet. La méthode appelée a pris une Action tant que paramètre et l’expression lambda transmise pour cette action a été précédée du mot clé async . Comme Stephen Cleary le souligne plus haut dans sa réponse, ceci est traité comme un async void , ce que MVC n’aime pas. Heureusement, ce paquet avait des versions * Async des mêmes méthodes. Le passage à l’utilisation de ceux-ci, avec quelques appels en aval au même package, a résolu le problème.

Je me rends compte que ce n’est pas une solution originale au problème, mais j’ai repassé ce fil à quelques resockets dans mes recherches en essayant de résoudre le problème car je pensais ne pas avoir d’appel async ou async , et je voulais pour aider quelqu’un d’autre à éviter cela.