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.
Любая помощь, идентифицирующая виновника, будет оценена.
Ответы
Ответ 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>
и я хотел помочь кому-то другому избежать этого.