extensions icon indicating copy to clipboard operation
extensions copied to clipboard

HttpClient.Timeout overrides the TotalRequestTimeout when using AddStandardResilienceHandler

Open martintmk opened this issue 2 years ago • 3 comments

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

martintmk avatar Nov 30 '23 12:11 martintmk

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.

rafal-mz avatar Dec 01 '23 08:12 rafal-mz

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

Few reasons why this is beneficial:

  • When Polly strategy is triggered, the resilience telemetry is reported alongside the timeout.
  • TaskCanelledException vs TimeoutRejectedException, the latter gives more details.
  • Saving extra allocation of CancellationTokenSource by HttpClient: https://source.dot.net/#System.Net.Http/System/Net/Http/HttpClient.cs,805

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.

martintmk avatar Dec 01 '23 15:12 martintmk

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.

iliar-turdushev avatar Apr 29 '24 15:04 iliar-turdushev