dotnet / extensions

This repository contains a suite of libraries that provide facilities commonly needed when creating production-ready applications.
MIT License
2.58k stars 740 forks source link

HttpClient.Timeout overrides the TotalRequestTimeout when using AddStandardResilienceHandler #4770

Closed martintmk closed 4 days ago

martintmk commented 9 months ago

Description

The HttpClient.Timeout interferes with the TotalRequestTimeout when using HttpClient with standard pipeline. If HttpClient.Timeout is less than TotalRequestTimeout, then the TotalRequestTimeout is ignored.

This causes mis-alignment and unexpected behavior for anyone setting the TotalRequestTimeout to a value greater than 100 seconds, which is the default for HttpClient.Timeout.

Calling AddStandardResilienceHandler should disable the HttpClient.Timeout by setting the value to Timeout.InfiniteTimeSpan, because the overall timeout is already enforced by standard pipeline. This also saves some allocations due to HttpClient not needing to create CancellationTokenSource.

Reproduction Steps

Execute this code:

var client = new ServiceCollection()
    .AddHttpClient("my-client", c => c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"))
    .AddStandardResilienceHandler()
    .Configure(options =>
    {
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(1);
        options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(150);
        options.Retry.ShouldHandle = _ => new ValueTask<bool>(watch.Elapsed < TimeSpan.FromSeconds(100));
        options.Retry.MaxRetryAttempts = int.MaxValue;
    })
    .Services.BuildServiceProvider()
    .GetRequiredService<IHttpClientFactory>()
    .CreateClient("my-client");

await client.GetStringAsync("/", default);

This should be successful. Instead, the TaskCancelledException is thrown.

Expected behavior

Above example should not throw exception.

Actual behavior

Above example throws TaskCancelledException.

Regression?

No response

Known Workarounds

Manually disable HttpClient timeout:

var client = new ServiceCollection()
    .AddHttpClient("my-client", c =>
    {
        c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com");
        c.Timeout = Timeout.InfiniteTimeSpan;
    })
    .AddStandardResilienceHandler();

Configuration

.NET SDK: Version: 8.0.100 Commit: 57efcf1350 Workload version: 8.0.100-manifests.6a1e483a

Runtime Environment: OS Name: Windows OS Version: 10.0.22621 OS Platform: Windows RID: win-x64 Base Path: C:\Program Files\dotnet\sdk\8.0.100\

Other information

No response

rafal-mz commented 9 months ago

Why is it better to use Polly's timeout policy implementation over built in HttpClient timeout? There is also a second possible solution to just make polly set the HttpClient.Timeout to the right value.

martintmk commented 9 months ago

Why is it better to use Polly's timeout policy implementation over built in HttpClient timeout?

Few reasons why this is beneficial:

There is also a second possible solution to just make polly set the HttpClient.Timeout to the right value.

This would lead to race conditions between Polly's timeout and HttpClient's timeout. Sometimes the timeout would be triggered by HttpClient, other times by Polly.

iliar-turdushev commented 4 months ago

The fix for current bug was reverted because it introduced the following bug #4924. When we fix #4924 we'll get back the fix for the current bug.