重新发送 HttpRequestMessage - 异常

Posted

技术标签:

【中文标题】重新发送 HttpRequestMessage - 异常【英文标题】:Re-Send HttpRequestMessage - Exception 【发布时间】:2013-08-01 17:21:13 【问题描述】:

我想多次发送完全相同的请求,例如:

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;

【讨论】:

这并不总是有效。如果您有一个没有任何内容的请求,它可以正常工作。但是,如果您尝试使用已使用的内容克隆请求,则会失败并显示“无法访问已处置的对象”。错误信息。 @G0tPwned 你是对的,当有内容时它不起作用。知道如何克隆内容吗? @Prabhu 如果对内容调用 LoadIntoBufferAsync,则可以保证内容缓冲在 HttpContent 对象内部。剩下的唯一问题是读取流并不会重置位置,所以需要ReadAsStreamAsync并设置流Position = 0。 @Skadoosh 我改进了 drahcir 的解决方案以解决请求有内容的情况。请参阅下面的答案。 @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;

编辑 05/02/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;

【讨论】:

在异步块中使用此代码时的警告词,您可以按原样使用代码导致线程死锁。一种更安全的模式是同时使用两种扩展方法async。第一种方法将调用Content = await request.Content.Clone()。第二种方法将调用await content.CopyToAsync(ms);Wait() 方法使此执行同步并在创建流时短暂锁定整个等待调用链。 仅供参考,根据下面的文档,StreamContent 确实 在提供的流上调用 Dispose,因此这是安全处置的。 docs.microsoft.com/en-us/dotnet/api/… 另请注意,异步方法定义实际上并不包含 async 关键字。【参考方案3】:

我正在传递Func&lt;HttpRequestMessage&gt; 的实例而不是HttpRequestMessage 的实例。 func 指向一个工厂方法,因此每次调用它时我都会收到一条全新的消息,而不是重复使用。

【讨论】:

@G0tPwned mediaingenuity.github.io/2013/09/25/… 尝试在没有委托处理程序包装器的情况下使用 Polly 实现这一点浪费了半个小时。如果没有处理程序,不建议使用此方法。【参考方案4】:

我有类似的问题,并以一种 hack 的方式解决了它,反射。

感谢开源!通过阅读源代码,原来HttpRequestMessage类中有一个私有字段_sendStatus,我所做的是在重用请求消息之前将其重置为0。它适用于 .NET Core,我希望微软不会永远重命名或删除它。 :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.");

【讨论】:

Mono 有 bool HttpRequestMessage.is_used 标志 :-)【参考方案5】:

AFAIK,HttpClient 只是 'HttpWebRequest' 的包装器,它使用流来发送/接收数据,因此无法重用请求,尽管克隆它/使其循环运行应该非常简单。

【讨论】:

以上是关于重新发送 HttpRequestMessage - 异常的主要内容,如果未能解决你的问题,请参考以下文章

将 JSON 字符串加载到 HttpRequestMessage

c# 如何正确传递 HttpRequestMessage 并在函数外返回 HttpRequestMessage 而不会泄漏

HttpRequestMessage.Content 带有无效和不可读的字符

HttpClient 标头与 HttpRequestMessage 标头

HttpRequestMessage.CreateResponse 抛出 ***Exception

HttpRequestMessage 多个自定义标头相互覆盖