This .NET Framework 4.8 implementation below allows you to
- have a singleton
HttpClient, and
- send a different timeout per request.
The way this works is, your singleton HttpClient will have an infinite timeout, but it'll use a custom HttpClientHandler that will hijack the default SendAsync behavior of your HttpClient to honor any custom "timeout property" set on the HttpRequestMessage you're trying to send. This timeout will then be enforced by a CancellationToken.
First you create a custom HttpClientHandler. (I called mine TimeoutHttpClientHandler.)
public class TimeoutHttpClientHandler : HttpClientHandler
{
private readonly TimeSpan _defaultTimeout;
public TimeoutHttpClientHandler(TimeSpan timeout)
{
_defaultTimeout = timeout;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
TimeSpan timeout = _defaultTimeout;
if (request.Properties != null
&& request.Properties.TryGetValue(HttpClientSingleton.TimeoutPropertyKey, out var value)
&& value is TimeSpan parsedTimeout)
{
timeout = parsedTimeout;
}
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
try
{
cts.CancelAfter(timeout);
return await base.SendAsync(request, cts?.Token ?? cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
when (!cancellationToken.IsCancellationRequested)
{
throw new TimeoutException();
}
}
}
}
Then you instantiate your singleton HttpClient with an infinite timespan timeout, using this handler:
public static class HttpClientSingleton
{
public const string TimeoutPropertyKey = "Timeout";
private static readonly Lazy<HttpClient> _lazyHttpClient = new Lazy<HttpClient>(() => CreateHttpClient());
public static HttpClient Instance
=> _lazyHttpClient.Value;
private static HttpClient CreateHttpClient()
{
//I arbitrarily chose 60 seconds here; you can do whatever
var handler = new TimeoutHttpClientHandler(TimeSpan.FromSeconds(60));
var client = new HttpClient(handler)
{
Timeout = Timeout.InfiniteTimeSpan
};
return client;
}
}
Then, per request, it assumes you will set the timeout like this on your HttpRequestMessage:
var httpRequest = new HttpRequestMessage(HttpMethod.Post, endpoint);
//Here is where I am setting the custom 90-second timeout, below
httpRequest.Properties[HttpClientSingleton.TimeoutPropertyKey] = TimeSpan.FromSeconds(90);
var response = await HttpClientSingleton.Instance.SendAsync(httpRequest).ConfigureAwait(false);
Timeoutis used to setCancelAfteron theCancellationTokenSourcebefore the async task is started (internally). So, even if you could change it afterwards, through some "trick", it would have no effect.