ti-enxame.com

API da Web + HttpClient: um módulo ou manipulador assíncrono concluído enquanto uma operação assíncrona ainda estava pendente

Estou escrevendo um aplicativo que proxies algumas solicitações HTTP usando a API da Web do ASP.NET e estou lutando para identificar a origem de um erro intermitente. Parece uma condição de corrida ... mas não tenho muita certeza.

Antes de entrar em detalhes, aqui está o fluxo de comunicação geral do aplicativo:

  • Cliente faz uma solicitação HTTP para Proxy 1.
  • Proxy 1 retransmite o conteúdo da solicitação HTTP para Proxy 2
  • Proxy 2 retransmite o conteúdo da solicitação HTTP para o Aplicativo Web de Destino
  • Target Web App responde à solicitação HTTP e a resposta é transmitida (transferência em partes) para o Proxy 2
  • Proxy 2 retorna a resposta para Proxy 1, que por sua vez responde à chamada original Cliente.

Os aplicativos de proxy são escritos na API da Web do ASP.NET RTM usando o .NET 4.5. O código para executar a retransmissão é assim:

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

O erro que ocorre intermitentemente é:

Um módulo ou manipulador assíncrono foi concluído enquanto uma operação assíncrona ainda estava pendente.

Esse erro geralmente ocorre nas primeiras solicitações para os aplicativos proxy, após as quais o erro não é visto novamente.

O Visual Studio nunca captura a exceção quando lançada. Mas o erro pode ser detectado no evento Global.asax Application_Error. Infelizmente, a exceção não tem rastreamento de pilha.

Os aplicativos de proxy estão hospedados nas Funções da Web do Azure.

Qualquer ajuda para identificar o culpado seria apreciada.

49
Gavin Osborn

Seu problema é sutil: o lambda async que você está passando para PushStreamContent está sendo interpretado como um async void (Porque o PushStreamContent construtor leva apenas Actions como parâmetros). Portanto, há uma condição de corrida entre a conclusão do módulo/manipulador e a conclusão do lambda async void.

PostStreamContent detecta o fechamento do fluxo e trata isso como o final de seu Task (completando o módulo/manipulador), então você só precisa ter certeza de que não há métodos async void que poderiam ainda será executado após o fluxo ser fechado. Os métodos async Task Estão OK, então isso deve corrigi-lo:

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

Se você deseja que seus proxies aumentem um pouco melhor, também recomendo livrar-se de todas as chamadas 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;
  }
}

Seu código anterior bloquearia um encadeamento para cada solicitação (até que os cabeçalhos sejam recebidos); usando async até o nível do seu controlador, você não bloqueará um thread durante esse período.

65
Stephen Cleary

Um modelo um pouco mais simples é que você pode simplesmente usar o HttpContents diretamente e passá-lo dentro do relé. Acabei de enviar uma amostra que ilustra como você pode confiar em solicitações e respostas de forma assíncrona e sem armazenar em buffer o conteúdo de uma maneira relativamente simples:

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

Também é benéfico reutilizar a mesma instância HttpClient, pois isso permite reutilizar conexões, quando apropriado.

4
Henrik Frystyk Nielsen

Gostaria de acrescentar um pouco de sabedoria para qualquer pessoa que tenha chegado aqui com o mesmo erro, mas todo o seu código parece bom. Procure por expressões lambda passadas para funções na árvore de chamadas de onde isso ocorre.

Eu estava recebendo esse erro em uma chamada JSON JavaScript para uma ação do controlador MVC 5.x. Tudo o que eu fazia na pilha foi definido async Task e chamado usando await.

No entanto, usando o recurso "Definir próxima declaração" do Visual Studio, pulei sistematicamente as linhas para determinar qual causou. Continuei pesquisando métodos locais até chegar a uma chamada em um pacote NuGet externo. O método chamado utilizou um Action como parâmetro e a expressão lambda transmitida para esta ação foi precedida pela palavra-chave async. Como Stephen Cleary aponta acima em sua resposta, isso é tratado como um async void, que o MVC não gosta. Felizmente, o pacote tinha * versões assíncronas dos mesmos métodos. A mudança para usá-los, juntamente com algumas chamadas posteriores para o mesmo pacote, corrigiu o problema.

Sei que essa não é uma solução nova para o problema, mas repassei esse tópico algumas vezes em minhas pesquisas, tentando resolver o problema, porque pensei que não tinha nenhum async void ou async <Action> chama, e eu queria ajudar alguém a evitar isso.

3
Dave Parker