postasync postasjsonasync c# asp.net asp.net-web-api dotnet-httpclient

postasync - postasjsonasync c#



Web Api+HttpClient: un módulo asincrónico o controlador completado mientras que una operación asíncrona estaba pendiente (3)

Estoy escribiendo una aplicación que transmite algunas solicitudes HTTP utilizando ASP.NET Web API y estoy luchando para identificar el origen de un error intermitente. Parece una condición de carrera ... pero no estoy del todo seguro.

Antes de entrar en detalles, aquí está el flujo de comunicación general de la aplicación:

  • El cliente realiza una solicitud HTTP al Proxy 1 .
  • Proxy 1 retransmite el contenido de la solicitud HTTP a Proxy 2
  • Proxy 2 retransmite el contenido de la solicitud HTTP a la aplicación web de destino
  • La aplicación web de destino responde a la solicitud HTTP y la respuesta se transmite por secuencias (transferencia fragmentada) a Proxy 2
  • Proxy 2 devuelve la respuesta al Proxy 1, que a su vez responde al cliente que realiza la llamada original.

Las aplicaciones Proxy están escritas en ASP.NET Web API RTM usando .NET 4.5. El código para realizar el relevo se ve así:

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

El error que ocurre intermitentemente es:

Un módulo o controlador asíncrono completado mientras que una operación asincrónica todavía estaba pendiente.

Este error generalmente ocurre en las primeras solicitudes a las aplicaciones proxy después de las cuales el error no se vuelve a ver.

Visual Studio nunca detecta la excepción cuando se lanza. Pero el error se puede detectar en el evento Global.asax Application_Error. Lamentablemente, la excepción no tiene Stack Trace.

Las aplicaciones proxy están alojadas en Roles web de Azure.

Cualquier ayuda para identificar al culpable sería apreciada.


Me gustaría agregar algo de sabiduría para cualquier persona que haya aterrizado aquí con el mismo error, pero todo su código parece estar bien. Busque cualquier expresión lambda pasada a funciones en el árbol de llamadas desde donde ocurre esto.

Estaba obteniendo este error en una llamada JavaScript JSON a una acción de controlador MVC 5.x. Todo lo que hacía arriba y abajo de la pila se definía como una async Task y se llamaba usando await .

Sin embargo, al utilizar la función "Establecer la siguiente declaración" de Visual Studio, omití sistemáticamente las líneas para determinar cuál lo causó. Seguí profundizando en los métodos locales hasta que llegué a una llamada en un paquete NuGet externo. El método llamado tomó una Action como parámetro y la expresión lambda pasada para esta Acción fue precedida por la palabra clave async . Como señala Stephen Cleary en su respuesta, esto se trata como un async void , que MVC no le gusta. Afortunadamente dicho paquete tenía * versiones Async de los mismos métodos. Pasar a usar esos, junto con algunas llamadas en sentido descendente al mismo paquete, solucionó el problema.

Me doy cuenta de que esta no es una solución novedosa al problema, pero pasé este hilo varias veces en mis búsquedas tratando de resolver el problema porque pensé que no tenía llamadas async void o async <Action> , y quería para ayudar a alguien más a evitar eso.


Su problema es sutil: la lambda async que está pasando a PushStreamContent está siendo interpretada como un async void (porque el constructor PushStreamContent solo toma Action s como parámetros). Entonces, hay una condición de carrera entre su módulo / manejador que completa y la finalización de ese async void lambda.

PostStreamContent detecta el cierre del flujo y lo trata como el final de su Task (completando el módulo / manejador), por lo que solo necesita asegurarse de que no existan métodos de async void que aún puedan ejecutarse después de que se cierre el flujo. async Task métodos de async Task están bien, así que esto debería solucionarlo:

private static PushStreamContent CopyContentStream(HttpResponseMessage sourceContent) { Func<Stream, Task> 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 desea que sus proxies escalen un poco mejor, también le recomiendo deshacerse de todas las llamadas de Result :

//Controller entry point. public async Task<HttpResponseMessage> 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; } }

Su código anterior bloquearía un hilo por cada solicitud (hasta que se reciban los encabezados); mediante el uso de async hasta su nivel de controlador, no bloqueará un hilo durante ese tiempo.


Un modelo un poco más simple es que puedes simplemente usar los HttpContents directamente y pasarlos dentro del relevo. Acabo de subir una muestra que ilustra cómo puede confiar tanto las solicitudes como las respuestas de forma asíncrona y sin almacenar el contenido de forma relativamente simple:

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

También es beneficioso reutilizar la misma instancia de HttpClient ya que esto le permite reutilizar las conexiones cuando corresponda.