34

I want to send the exact same request more than once, for example:

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

Sending the request for a second time will throw an exception with the message:

The request message was already sent. Cannot send the same request message multiple times.

Is there a way to "clone" the request so that I can send again?

My real code has more variables set on the HttpRequestMessage than in the example above, variables like headers and request method.

DavidS
  • 2,179
  • 4
  • 26
  • 44
Drahcir
  • 11,772
  • 24
  • 86
  • 128

5 Answers5

24

I wrote the following extension method to clone the request.

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;
}
Drahcir
  • 11,772
  • 24
  • 86
  • 128
  • 10
    This doesn't always work. If you have a request without any content, it works fine. However, if you try to clone a request with content that has been used already, it will fail with 'Cannot access a disposed object.' error message. – Skadoosh Feb 18 '14 at 21:06
  • @G0tPwned You're right it doesn't work when there is content. Any idea how we can clone with content? – Prabhu Jul 30 '14 at 20:07
  • 5
    @Prabhu If you call LoadIntoBufferAsync on the content, you can guarantee that the content is buffered inside the HttpContent object. The only problem remaining is that reading the stream does not reset the position, so you need to ReadAsStreamAsync and set the stream Position = 0. – Darrel Miller Aug 02 '14 at 14:11
  • @Skadoosh I improved drahcir's solution to address the case where the request has content. See my answer below. – desautelsj Dec 12 '17 at 16:48
  • @Skadoosh You need to clone the request *before* sending it, because SendAsync will dispose the request's content – Paulo Sep 10 '18 at 14:43
22

Here is an improvement to the extension method proposed by @drahcir. The improvement is to ensure the content of the request is cloned as well as the request itself:

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

Edit 05/02/18: here's Async version

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;
}
desautelsj
  • 3,587
  • 4
  • 37
  • 55
  • 6
    Word of warning when using this code inside an async block, you can cause thread **deadlocks** by using the code as-is. A safer pattern is to make both extension methods `async`. This first method would call `Content = await request.Content.Clone()`. The second method would call `await content.CopyToAsync(ms);`. The `Wait()` method makes this execution synchronous and locking the entire await call chain briefly while the stream is created. – Sixto Saez Feb 12 '18 at 16:53
  • Just FYI, according to the doc below, `StreamContent` *does* call `Dispose` on the stream it is provided, so this is disposal-safe. https://learn.microsoft.com/en-us/dotnet/api/system.net.http.streamcontent.-ctor?view=net-5.0#System_Net_Http_StreamContent__ctor_System_IO_Stream_ – Josh Jun 10 '21 at 14:04
  • 1
    Also note that the async method definitions do not actually include the `async` keyword. – Josh Jun 10 '21 at 14:05
14

I am passing around an instance of Func<HttpRequestMessage> instead of an instance of HttpRequestMessage. The func points to a factory method so I get a brand new message each time it is called instead of re-using.

Nick
  • 6,366
  • 5
  • 43
  • 62
  • 2
    @G0tPwned http://mediaingenuity.github.io/2013/09/25/putting-the-func-in-dot-net.html – Nick Oct 05 '14 at 08:38
  • 2
    Trying to implement this with Polly without a delegate handler wrapper wasted half an hour. This method is not recommended without the handler. – WULF Jun 17 '20 at 17:37
8

I have similar problem and resolved it in a hack way, reflection.

Thanks for open source! By reading the source code, it turns out there's a private field _sendStatus in HttpRequestMessage class, what I did is to reset it to 0 before reusing the request message. It works in .NET Core and I wish Microsoft would not rename or remove it for ever. :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.");
}
Ricky
  • 10,044
  • 3
  • 26
  • 31
0

AFAIK, HttpClient is just a wrapper around 'HttpWebRequest's, which use streams to send/receeive data, making it impossible to re-use the request, although it should be pretty simple to juste clone it/make this in a loop.