#c# #httpclient #delegatinghandler
#c# #.net #dotnet-httpclient
Вопрос:
Я хочу отправить один и тот же запрос более одного раза, например:
HttpClient client = new HttpClient();
HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, "http://example.com");
await client.SendAsync(req, HttpCompletionOption.ResponseContentRead);
await client.SendAsync(req, HttpCompletionOption.ResponseContentRead);
Отправка запроса во второй раз вызовет исключение с сообщением:
Сообщение с запросом уже было отправлено. Невозможно отправить одно и то же сообщение с запросом несколько раз.
Есть ли способ «клонировать» запрос, чтобы я мог отправить его снова?
В моем реальном коде установлено больше переменных HttpRequestMessage
, чем в примере выше, таких переменных, как заголовки и метод запроса.
Ответ №1:
Я написал следующий метод расширения для клонирования запроса.
public static HttpRequestMessage Clone(this HttpRequestMessage req)
{
HttpRequestMessage clone = new HttpRequestMessage(req.Method, req.RequestUri);
clone.Content = req.Content;
clone.Version = req.Version;
foreach (KeyValuePair<string, object> prop in req.Properties)
{
clone.Properties.Add(prop);
}
foreach (KeyValuePair<string, IEnumerable<string>> header in req.Headers)
{
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return clone;
}
Комментарии:
1. Это не всегда работает. Если у вас есть запрос без какого-либо содержимого, он работает нормально. Однако, если вы попытаетесь клонировать запрос с содержимым, которое уже использовалось, произойдет сбой с сообщением об ошибке «Невозможно получить доступ к удаленному объекту».
2. @G0tPwned Вы правы, это не работает, когда есть контент. Есть идеи, как мы можем клонировать содержимое?
3. @Prabhu Если вы вызываете LoadIntoBufferAsync для содержимого, вы можете гарантировать, что содержимое буферизуется внутри объекта HttpContent. Единственная оставшаяся проблема заключается в том, что чтение потока не сбрасывает позицию, поэтому вам нужно ReadAsStreamAsync и установить позицию потока = 0.
4. @Skadoosh Я улучшил решение drahcir, чтобы учесть случай, когда запрос содержит содержимое. Смотрите мой ответ ниже.
5. @Skadoosh Вам нужно клонировать запрос перед его отправкой, потому что SendAsync удалит содержимое запроса
Ответ №2:
Вот улучшение метода расширения, предложенного @drahcir. Улучшение заключается в обеспечении клонирования содержимого запроса, а также самого запроса:
public static HttpRequestMessage Clone(this HttpRequestMessage request)
{
var clone = new HttpRequestMessage(request.Method, request.RequestUri)
{
Content = request.Content.Clone(),
Version = request.Version
};
foreach (KeyValuePair<string, object> prop in request.Properties)
{
clone.Properties.Add(prop);
}
foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
{
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return clone;
}
public static HttpContent Clone(this HttpContent content)
{
if (content == null) return null;
var ms = new MemoryStream();
content.CopyToAsync(ms).Wait();
ms.Position = 0;
var clone = new StreamContent(ms);
foreach (KeyValuePair<string, IEnumerable<string>> header in content.Headers)
{
clone.Headers.Add(header.Key, header.Value);
}
return clone;
}
Редактировать 02.05.18: вот асинхронная версия
public static async Task<HttpRequestMessage> CloneAsync(this HttpRequestMessage request)
{
var clone = new HttpRequestMessage(request.Method, request.RequestUri)
{
Content = await request.Content.CloneAsync().ConfigureAwait(false),
Version = request.Version
};
foreach (KeyValuePair<string, object> prop in request.Properties)
{
clone.Properties.Add(prop);
}
foreach (KeyValuePair<string, IEnumerable<string>> header in request.Headers)
{
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return clone;
}
public static async Task<HttpContent> CloneAsync(this HttpContent content)
{
if (content == null) return null;
var ms = new MemoryStream();
await content.CopyToAsync(ms).ConfigureAwait(false);
ms.Position = 0;
var clone = new StreamContent(ms);
foreach (KeyValuePair<string, IEnumerable<string>> header in content.Headers)
{
clone.Headers.Add(header.Key, header.Value);
}
return clone;
}
Комментарии:
1. Предупреждение при использовании этого кода внутри асинхронного блока вы можете вызвать взаимоблокировки потоков, используя код как есть. Более безопасный шаблон — создать оба метода расширения
async
. Этот первый метод будет вызыватьContent = await request.Content.Clone()
. Второй метод будет вызыватьawait content.CopyToAsync(ms);
.Wait()
Метод делает это выполнение синхронным и ненадолго блокирует всю цепочку ожидающих вызовов во время создания потока.2.К вашему сведению, согласно приведенному ниже документу,
StreamContent
вызываетсяDispose
в потоке, который он предоставляет, так что это безопасно для удаления. learn.microsoft.com/en-us/dotnet/api /…3. Также обратите внимание, что определения асинхронного метода фактически не включают
async
ключевое слово.
Ответ №3:
Я передаю экземпляр Func<HttpRequestMessage>
вместо экземпляра HttpRequestMessage
. Функция указывает на заводской метод, поэтому я получаю совершенно новое сообщение каждый раз, когда оно вызывается вместо повторного использования.
Комментарии:
1. @G0tPwned mediaingenuity.github.io/2013/09/25 /…
2. Попытка реализовать это с помощью Polly без оболочки обработчика делегата потратила полчаса впустую. Этот метод не рекомендуется без обработчика.
Ответ №4:
У меня похожая проблема, и я решил ее хакерским способом, размышлением.
Спасибо за открытый исходный код! Читая исходный код, оказывается, есть частное поле _sendStatus
в HttpRequestMessage
классе, что я сделал, чтобы сбросить ее 0
перед повторным использованием сообщения запроса. Это работает в .NET Core, и я бы хотел, чтобы Microsoft не переименовывала и не удаляла его навсегда. :p
// using System.Reflection;
// using System.Net.Http;
// private const string SEND_STATUS_FIELD_NAME = "_sendStatus";
private void ResetSendStatus(HttpRequestMessage request)
{
TypeInfo requestType = request.GetType().GetTypeInfo();
FieldInfo sendStatusField = requestType.GetField(SEND_STATUS_FIELD_NAME, BindingFlags.Instance | BindingFlags.NonPublic);
if (sendStatusField != null)
sendStatusField.SetValue(request, 0);
else
throw new Exception($"Failed to hack HttpRequestMessage, {SEND_STATUS_FIELD_NAME} doesn't exist.");
}
Комментарии:
1. У Mono есть
bool HttpRequestMessage.is_used
флаг 🙂
Ответ №5:
AFAIK, HttpClient — это просто оболочка вокруг HttpWebRequest, которая использует потоки для отправки / получения данных, что делает невозможным повторное использование запроса, хотя должно быть довольно просто просто клонировать его / сделать это в цикле.