在Polly重试策略中重复使用HttpRequestMessage

4
一个HttpRequestMessage对象只能使用一次;尝试再次使用相同的对象将引发异常。我正在使用Polly重试一些请求,并且遇到了这个问题。我知道如何克隆一个请求,在SO上有很多例子,但是我无法弄清楚如何克隆一个请求并在Polly重试时发送该新请求。我该怎么做?
这是我的策略,供参考。这是一个Xamarin应用程序。如果网络出现故障,我希望重新尝试几次,并且如果响应未经授权,我希望使用保存的凭据重新进行身份验证,并再次尝试原始请求。
public static PolicyWrap<HttpResponseMessage> RetryPolicy
{
    get => WaitAndRetryPolicy.WrapAsync(ReAuthPolicy);
}

private static IAsyncPolicy WaitAndRetryPolicy
{
    get => Policy.Handle<WebException>().WaitAndRetryAsync(4, _ => TimeSpan.FromSeconds(2));
}

private static IAsyncPolicy<HttpResponseMessage> ReAuthPolicy
{
    get => Policy.HandleResult<HttpResponseMessage>(x => x.StatusCode == HttpStatusCode.Unauthorized)
        .RetryAsync((_, __) => CoreService.LogInWithSavedCredsAsync(true));
}

这种方法无法正常工作是因为 HttpRequestMessage 被重复使用了,但这正是我想要实现的:

var request = new HttpRequestMessage(HttpMethod.Post, "some_endpoint")
{
    Content = new StringContent("some content")
};

request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

var policyResponse = await ConnectivityHelper.RetryPolicy
    .ExecuteAndCaptureAsync(() => _client.SendAsync(request)).ConfigureAwait(false);

// handle outcome

2
请查看此问题,特别是此答案。将重试放在DelegatingHandler中可以避免无法重用的问题。如果使用ASPNET Core并且可以等待2.1 RTM,请考虑新的IHttpClientFactory。如果是这样,请参阅Polly文档 - mountain traveller
@mountaintraveller 谢谢,我之前没看到过这个。不过我不确定如何将其与我已有的策略结合起来,我很难理解 Polly 策略应该如何协同工作。(这是一个 Xamarin 应用程序,尽管我认为这个问题足够通用,所以我没有包括它。) - vaindil
1
Polly文档中关于IHttpClientFactory的讨论涉及使用多个策略的情况。这包括在没有IHttpClientFactory的DelegatingHandler方法中应用多个策略的情况。有关该概念的讨论,请参见Steve Gordon的博客。如果使用的是ASPNET Core 2.1之前的版本,则必须手动创建DelegatingHandlers链。 - mountain traveller
如果您的查询通常涉及组合 Polly 策略时的行为,请参阅 Polly wiki on PolicyWrap - mountain traveller
@mountaintraveller 哦,我现在明白了,我脑海中缺失的部分是 DelegatingHandler 的工作原理。从我的理解来看,它本质上是 HttpClient 的中间件,对吗?因此,我只需一次性配置整个过程并将其传递给 HttpClient 构造函数,而不是每次手动调用我的 Polly 策略?这使事情变得更加容易。现在我的问题是如何将 DelegatingHandler 方法与 ExecuteAndCaptureAsync 的行为相结合。顺便说一下,这是一个 Xamarin 应用程序。 - vaindil
1
是的,DelegatingHandler是HttpClient的中间件。无法将ExecuteandCaptureAsync()与之组合 - 也就是说,在DelegatingHandler内部。ExecuteandCaptureAsync()会更改执行的返回类型;但是中间件无法做到这一点。 - mountain traveller
2个回答

2
如果重用了 HttpRequestMessage,抛出 InvalidOperationException 的代码是 HttpClient 中的验证步骤。 源代码链接
private static void CheckRequestMessage(HttpRequestMessage request)
{
    if (!request.MarkAsSent())
    {
        throw new InvalidOperationException(SR.net_http_client_request_already_sent);
    }
}

源代码链接

internal bool MarkAsSent()
{
    return Interlocked.Exchange(ref sendStatus, messageAlreadySent) == messageNotYetSent;
}

您可以将Polly重试策略放入DelegatingHandler中,这样就行了。它还提供了良好的关注分离。如果将来您想要不重试或更改重试行为,只需删除DelegatingHandler或更改它即可。请注意处理HttpRequestMessage和中间HttpResponseMessage对象。这是我使用的一个良好结果的示例(retry policy)。
您的问题是开放式的,通常SO不适合那些(see)。但是,这里我介绍一种“反应式”方法,因为它在token到期之前一直使用它,并获取新的token。请注意,这不会通过使用令牌ttl产生401。
# gets token with its ttl
tokenService: iTokenService
    # use retry policy in DH here
    httpClient
    string getTokenAsync():
        # calls out for token
        # note: tokens typically have a ttl

# returns cached token till its tll, or gets a new token which is then cached
cachedTokenService: iCachedTokenService
    tokenCached
    tokenTtl
    iTokenService
    
    string getTokenAsync():
        # returns tokenCached or gets a new token based on ttl
        # note: fetches with some buffer before ttl to avoid failures on edge
        # note: buffer as 2x http timeout is good enough

# DH that adds the cached token to the outgoing "work" request
tokenHandler: delegatingHandler
    iCachedTokenService
    task<response> sendAsync(request, ct):
        # gets token, and adds token to request header

# worker service
workService: iWorkService
    # uses tokenHandler DH
    httpClient
    workAsync():
        # ...

0

最简单的解决方案是在ExecuteAndCaptureAsync委托中创建HttpRequestMessage,而不是重复使用它。换句话说,要重新创建它:

var policyResponse = await ConnectivityHelper.RetryPolicy
    .ExecuteAndCaptureAsync(async () => {
    var request = new HttpRequestMessage(HttpMethod.Post, "some_endpoint")
    {
        Content = new StringContent("some content", Encoding.UT8, "application/json")
    };
    return await _client.SendAsync(request)).ConfigureAwait(false);
});

或者更喜欢使用PostAsync而不是SendAsync

var policyResponse = await ConnectivityHelper.RetryPolicy.ExecuteAndCaptureAsync(
  async () => 
    await _client.PostAsync("some_endpoint", 
         new StringContent("some content", Encoding.UT8, "application/json"))
    .ConfigureAwait(false)
});

我理解。但我的观点仍然站得住脚。 - hIpPy
@hlpPy 抱歉,我不明白你的评论的意义。这是一个四年前的帖子。原始发帖人直接使用了策略,而不是通过建议的 HttpClient 集成。所以,你是想让我修改我的帖子还是怎么样? - Peter Csala
只是你提到的解决方案是一种反模式。我从谷歌上来到这里,所以当时添加了我的答案,并在你的答案上发表评论,以引导社区远离它。我在那里字符不够用,"polly retry delegatinghandler" 的意思是在 delegatingHandler 中使用 polly retry;可以使用带有重试 polly 策略的 RetryPolicyHandler(请参见 https://github.com/rmandvikar/delegating-handlers/blob/1f99bd7919e3883121785181702a18dcd2cc5d41/src/rm.DelegatingHandlers/ExponentialBackoffWithJitterRetryHandler.cs)或者 PolicyHttpMessageHandler 也可以。 - hIpPy
@hIpPy 我不同意你的“反模式”说法。 Polly 是一个通用的弹性库,可用于使用策略/策略装饰任何方法。 它也可以用于 HttpClient。 我提出的解决方案将重试显式地应用于 http 调用。 这意味着有意将调用包装到已知被调用端应该是幂等的重试中。 另一方面,每当您使用重试装饰整个 HttpClient 时,无论动词如何,您都会装饰所有调用。 并非所有下游以幂等的方式实现每个动词。 - Peter Csala
@hIpPy 由于您收到了一个HttpClient,策略/政策是由底层的DelegateHandler代表您调用的,因此很容易忘记给定的Http调用被包装在重试中。如果下游系统没有为该特定VERB实现幂等性,则可能会出现不需要的副作用。因此,从这个角度来看,“AddPolicyHandler”更容易出错。请在发表声明时谨慎。 - Peter Csala
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接