Web Api + HttpClient: асинхронный модуль или обработчик завершен, пока асинхронная операция еще не выполнена - программирование
Подтвердить что ты не робот

Web Api + HttpClient: асинхронный модуль или обработчик завершен, пока асинхронная операция еще не выполнена

Я пишу приложение, которое проксирует некоторые HTTP-запросы, используя веб-API ASP.NET, и я изо всех сил пытаюсь определить источник прерывистой ошибки. Это похоже на состояние гонки... но я не совсем уверен.

Прежде чем я расскажу подробнее, это общий коммуникационный поток приложения:

  • Клиент делает HTTP-запрос прокси 1.
  • Прокси 1 перенаправляет содержимое HTTP-запроса на Proxy 2
  • Прокси 2 передает содержимое HTTP-запроса в целевое веб-приложение
  • Целевое веб-приложение отвечает на HTTP-запрос, и ответ передается (передача по каналам) в Proxy 2
  • Прокси 2 возвращает ответ Прокси 1, который в свою очередь отвечает на исходный вызов Клиент.

Приложения Proxy записываются в RTM веб-API ASP.NET с использованием .NET 4.5. Код для выполнения реле выглядит так:

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

Ошибка, возникающая с перерывами:

Асинхронный модуль или обработчик завершен, пока асинхронная операция еще не выполнена.

Эта ошибка обычно возникает при первых нескольких запросах к прокси-приложениям, после чего ошибка больше не отображается.

Visual Studio никогда не ловит Исключение при броске. Но ошибка может быть обнаружена в событии Global.asax Application_Error. К сожалению, в Exception нет трассировки стека.

Прокси-приложения размещаются в ролях Azure Web.

Любая помощь, идентифицирующая виновника, будет оценена.

4b9b3361

Ответ 1

Ваша проблема тонкая: async lambda, которую вы переходите на PushStreamContent, интерпретируется как async void (потому что PushStreamContent конструктор принимает только Action как параметры). Итак, есть условие гонки между вашим модулем/обработчиком и завершением этого async void lambda.

PostStreamContent обнаруживает закрытие потока и рассматривает его как конец своего Task (завершение модуля/обработчика), поэтому вам просто нужно быть уверенным, что нет методов async void, которые все еще могут выполняться после того, как поток закрыто. async Task методы в порядке, поэтому это должно исправить:

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

Если вы хотите, чтобы ваши прокси масштабировались немного лучше, я также рекомендую избавиться от всех вызовов 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;
  }
}

Ваш прежний код блокирует один поток для каждого запроса (до получения заголовков); используя async вплоть до уровня вашего контроллера, вы не будете блокировать поток за это время.

Ответ 2

Немного более простая модель заключается в том, что вы можете просто напрямую использовать HttpContents и передавать их внутри реле. Я просто загрузил образец, иллюстрирующий, как можно асинхронно и как запросы, так и ответы и без буферизации содержимого относительно просто:

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

Также полезно повторно использовать один и тот же экземпляр HttpClient, поскольку это позволяет вам повторно использовать соединения, если это необходимо.

Ответ 3

Я хотел бы добавить некоторую мудрость для всех, кто приземлился здесь с той же ошибкой, но весь ваш код кажется прекрасным. Найдите любые лямбда-выражения, переданные в функции по дереву вызовов, откуда это происходит.

Я получал эту ошибку при вызове JavaScript JSON к действию контроллера MVC 5.x. Все, что я делал вверх и вниз по стеку, было определено async Task и вызвано с помощью await.

Однако, используя функцию Visual Studio "Установить следующий оператор", я систематически пропускал строки, чтобы определить, какой из них вызвал. Я продолжал бурить локальные методы, пока не получил вызов во внешний пакет NuGet. Вызываемый метод принял Action как параметр, и lambda-выражение, переданное для этого Action, предшествовало ключевое слово async. Как подчеркивает Стивен Клири в своем ответе, это рассматривается как async void, который MVC не нравится. К счастью, в пакете были версии Async тех же методов. Переключение на их использование, а также некоторые последующие вызовы в тот же пакет исправили проблему.

Я понимаю, что это не новое решение проблемы, но я несколько раз проходил этот поток в своих поисках, пытаясь решить проблему, потому что я думал, что у меня нет вызовов async void или async <Action> и я хотел помочь кому-то другому избежать этого.