Re-Send HttpRequestMessage - Exception
Asked Answered
F

5

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.

Fabrianne answered 1/8, 2013 at 17:21 Comment(0)
F
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;
}
Fabrianne answered 2/8, 2013 at 10:18 Comment(5)
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.Luce
@G0tPwned You're right it doesn't work when there is content. Any idea how we can clone with content?Diver
@Diver 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.Saxony
@Luce I improved drahcir's solution to address the case where the request has content. See my answer below.Josefajosefina
@Luce You need to clone the request before sending it, because SendAsync will dispose the request's contentCalyptrogen
J
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;
}
Josefajosefina answered 3/9, 2017 at 17:49 Comment(3)
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.Fireside
Just FYI, according to the doc below, StreamContent does call Dispose on the stream it is provided, so this is disposal-safe. learn.microsoft.com/en-us/dotnet/api/…Approximate
Also note that the async method definitions do not actually include the async keyword.Approximate
D
16

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.

Degrease answered 19/8, 2013 at 17:0 Comment(3)
@G0tPwned mediaingenuity.github.io/2013/09/25/…Degrease
Trying to implement this with Polly without a delegate handler wrapper wasted half an hour. This method is not recommended without the handler.Sclerite
Nice, works perfect for me!Kiarakibble
M
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.");
}
Mazman answered 20/4, 2017 at 7:10 Comment(1)
Mono has bool HttpRequestMessage.is_used flag :-)Baleen
G
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.

Geri answered 1/8, 2013 at 17:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.