Повторно отправить HttpRequestMessage — исключение

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