如何在Flurl.Http中使用Polly?

7

目前我有这个请求:

await url
    .SetQueryParams(queryString)
    .SetClaimsToken()
    .GetJsonAsync<T>()

我希望现在开始使用Polly来处理重试并提供更好的用户体验(https://github.com/App-vNext/Polly)。例如,由于网络不佳,在第一次尝试时不要“挂断”用户。这是我试图使用的示例:

int[] httpStatusCodesWorthRetrying = { 408, 500, 502, 503, 504 };
Policy
    .Handle<HttpException>()
    .OrResult<HttpResponse>(r => httpStatusCodesWorthRetrying.Contains(r.StatusCode))
    .WaitAndRetryAsync(new[] {
                    TimeSpan.FromSeconds(1),
                    TimeSpan.FromSeconds(2),
                    TimeSpan.FromSeconds(3)
                })
    .ExecuteAsync( await url... )

但是它需要HttpResponse作为返回类型。从我的Flurl示例中可以看出,它返回的是T,即使它是一个HttpResponseT只是用于反序列化StringContent的类型。

由于我在PCL中使用它,无法引用System.Web,因此这个第一个示例根本不起作用。所以我试了这个:

Policy
    .HandleResult(HttpStatusCode.InternalServerError)
    .OrResult(HttpStatusCode.BadGateway)
    .OrResult(HttpStatusCode.BadRequest)
    .WaitAndRetryAsync(new[] {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(2),
        TimeSpan.FromSeconds(3)
    })
    .ExecuteAsync(async () =>
    {
        await url...
    });

但是这个方法也不起作用,因为Polly期望返回类型为HttpStatusCode。所以我的问题是:我该如何告诉Polly处理那些HttpStatusCode,同时允许我返回类型T

您还可以注册全局 Polly 策略,使 Flurl 在每次调用时默认使用它。 - Todd Menier
https://stackoverflow.com/questions/51770071/what-are-the-http-codes-to-automatically-retry-the-request 这个链接提供了一个稍微不同的HTTP状态码列表,适用于自动重试的情况 - 408,425,429,500,502-504。 - undefined
3个回答

20

您不需要放弃使用像GetJsonAsync<T>()这样的便捷方法,因为Flurl会在非2XX响应(或者根据您的配置)时抛出异常,这应该使其与Polly非常好地配合使用。只需从原始代码中删除.Handle<HttpException>.OrResult<HttpResponse>部分,并改为处理FlurlHttpException即可:

T poco = await Policy
    .Handle<FlurlHttpException>(ex => httpStatusCodesWorthRetrying.Contains((int)ex.Call.Response.StatusCode))
    .WaitAndRetryAsync(...)
    .ExecuteAsync(() => url
        .SetQueryParams(queryString)
        .SetClaimsToken()
        .GetJsonAsync<T>());

以下是进一步简化的建议:

T poco = await Policy
    .Handle<FlurlHttpException>(IsWorthRetrying)
    .WaitAndRetryAsync(...)
    .ExecuteAsync(() => url
        .SetQueryParams(queryString)
        .SetClaimsToken()
        .GetJsonAsync<T>());

private bool IsWorthRetrying(FlurlHttpException ex) {
    switch ((int)ex.Call.Response.StatusCode) {
        case 408:
        case 500:
        case 502:
        case 504:
            return true;
        default:
            return false;
    }
}

看起来不错,托德!我也曾想过这种方法。一开始我没有推荐,因为我不确定所有的flurl调用都保证返回FlurlHttpException。例如,https://github.com/tmenier/Flurl/blob/master/src/Flurl.Http.Shared/FlurlClient.cs#L201(它是公共的)没有陷阱吗?但同意这个模式与`.GetJsonAsync<T>())`非常好。顺便说一句,很棒的库。 - mountain traveller
如果调用完成并且响应包含非成功状态,则“FlurlHttpException”几乎是肯定的。至少这是预期的、记录下来的行为。但如果这是一个问题,保留其他Polly配置肯定不会有害。 (谢谢,你的库看起来也很棒!它给了我在Flurl中添加重试的想法 :)) - Todd Menier
@mountaintraveller 顺便说一下,你标记的那行代码并没有捕获错误,因为它在 FlurlMessageHandler 中更深层次的被捕获/重新抛出为 FlurlHttpExcepton - Todd Menier
@ToddMenier 不错!我之前没有注意到默认工厂在HttpClient上注册了自定义的HttpMessageHandler。这个https://github.com/tmenier/Flurl/blob/master/src/Flurl.Http.Shared/Configuration/FlurlMessageHandler.cs#L25真是一篇很好的文章! - mountain traveller
@ToddMenier,谢谢,我找到了问题所在,我在Wait之前添加了OrResult,并将其更改为HTTPResponseMessage作为返回类型。顺便说一下,IsWorthReTrying需要对.Call进行空值检查,如果url无效,我会得到空异常。 - Robin
显示剩余3条评论

11

Polly可以将通过策略执行的委托返回的任何值解释为故障。但是,正如您所观察到的,在您发布的示例中调用.GetJsonAsync<T>()

await url
    .SetQueryParams(queryString)
    .SetClaimsToken()
    .GetJsonAsync<T>()

返回了T。这个调用通过直接进行Json反序列化到T,隐藏了HttpResponseMessage

你需要使用flurl中返回大约HttpResponseMessage的重载。我没有使用过flurl,但是这个重载返回Task<HttpResponseMessage>看起来很有前途。你可以尝试执行以下操作:

List<int> httpStatusCodesWorthRetrying = new List<int>(new[] {408, 500, 502, 503, 504});
HttpResponseMessage response = await Policy
    .Handle<HttpRequestException>() 
    .Or<OtherExceptions>() // add other exceptions if you find your call may throw them, eg FlurlHttpException
    .OrResult<HttpResponseMessage>(r => httpStatusCodesWorthRetrying.Contains((int)r.StatusCode))
    .WaitAndRetryAsync(new[] {
                    TimeSpan.FromSeconds(1),
                    TimeSpan.FromSeconds(2),
                    TimeSpan.FromSeconds(3)
                })
    .ExecuteAsync(() => 
       url
        .SetQueryParams(queryString)
        .SetClaimsToken()
        .GetAsync()
    );

T responseAsT = await Task.FromResult(response).ReceiveJson<T>();

在原始调用.GetJsonAsync<T>()的Flurl源代码这里与替换为.GetAsync();的源代码这里进行比较,建议最后调用.ReceiveJson<T>()。当然,你也可以将其包装成一个简洁的扩展帮助方法,例如:

async T GetJsonAsyncResiliently<T>(this IFlurlClient client, Policy policy) // OR (if preferred): this Url url instead of IFlurlClient client
{
    return await Task.FromResult(policy.ExecuteAsync(() => client.GetAsync())).ReceiveJson<T>();
}

编辑:我可能在指向IFlurlClient上的方法时指错了flurl过载函数。不过,在Urlstring上也有一组平行的扩展方法存在于flurl中,因此相同的原则适用。


@eestein 在下载了 Flurl 后进行了一些小的语法调整。这样做是否解决了你的问题? - mountain traveller
谢谢你的回答,我现在要测试一下! - eestein
谢谢,我能够使用你的例子来调整我的代码! :) - eestein

5

通过设置可配置Polly的HttpClientFactory和创建自定义的HttpClientFactory来配置Flurl:

public class MyCustomHttpClientFactory : DefaultHttpClientFactory, IMyCustomHttpClientFactory
{
    private readonly HttpClient _httpClient;
    public MyCustomHttpClientFactory(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    public override HttpClient CreateHttpClient(HttpMessageHandler handler)
    {
        return _httpClient;
    }
}

ConfigureServices中注册该服务:

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddHttpClient<IMyCustomHttpClientFactory, MyCustomHttpClientFactory>()
        .SetHandlerLifetime(...)
        .AddPolicyHandler(....);
}

将该工厂分配给Flurl:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // Get HttpClientFactory and Configure Flurl to use it.
    var factory = (IMyCustomHttpClientFactory)app.ApplicationServices.GetService(typeof(IMyCustomHttpClientFactory));
    FlurlHttp.Configure((settings) => settings.HttpClientFactory = factory);
}

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