Skip to content

Instantly share code, notes, and snippets.

@thomaslevesque
Last active December 1, 2023 13:08
Show Gist options
  • Star 54 You must be signed in to star a gist
  • Fork 13 You must be signed in to fork a gist
  • Save thomaslevesque/b4fd8c3aa332c9582a57935d6ed3406f to your computer and use it in GitHub Desktop.
Save thomaslevesque/b4fd8c3aa332c9582a57935d6ed3406f to your computer and use it in GitHub Desktop.
TimeoutHandler for smarter timeout handling with HttpClient

TimeoutHandler for smarter timeout handling with HttpClient

This code illustrates the blog article Better timeout handling with HttpClient.

Key features:

  • control the timeout per request, rather than globally for all requests
  • throw a more sensible exception (TimeoutException) when a timeout occurs, instead of the usual OperationCanceledException
public static class HttpRequestExtensions
{
private const string TimeoutPropertyKey = "RequestTimeout";
public static void SetTimeout(this HttpRequestMessage request, TimeSpan? timeout)
{
if (request == null) throw new ArgumentNullException(nameof(request));
request.Properties[TimeoutPropertyKey] = timeout;
}
public static TimeSpan? GetTimeout(this HttpRequestMessage request)
{
if (request == null) throw new ArgumentNullException(nameof(request));
if (request.Properties.TryGetValue(TimeoutPropertyKey, out var value) && value is TimeSpan timeout)
return timeout;
return null;
}
}
public class TimeoutHandler : DelegatingHandler
{
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(100);
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
using (var cts = GetCancellationTokenSource(request, cancellationToken))
{
try
{
return await base.SendAsync(request, cts?.Token ?? cancellationToken);
}
catch(OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
throw new TimeoutException();
}
}
}
private CancellationTokenSource GetCancellationTokenSource(HttpRequestMessage request, CancellationToken cancellationToken)
{
var timeout = request.GetTimeout() ?? DefaultTimeout;
if (timeout == Timeout.InfiniteTimeSpan)
{
// No need to create a CTS if there's no timeout
return null;
}
else
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeout);
return cts;
}
}
}
async Task TestAsync()
{
var handler = new TimeoutHandler
{
DefaultTimeout = TimeSpan.FromSeconds(10),
InnerHandler = new HttpClientHandler()
};
using (var cts = new CancellationTokenSource())
using (var client = new HttpClient(handler))
{
client.Timeout = Timeout.InfiniteTimeSpan;
var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:8888/");
// Uncomment to test per-request timeout
//request.SetTimeout(TimeSpan.FromSeconds(5));
// Uncomment to test that cancellation still works properly
//cts.CancelAfter(TimeSpan.FromSeconds(2));
using (var response = await client.SendAsync(request, cts.Token))
{
Console.WriteLine(response.StatusCode);
}
}
}
@tvanbaak
Copy link

Why not just subclass the derived class at that point then?

That couples the timeout functionality to the other property that the subclass implemented, which may not be desirable. Alternatively, I may not even be able to subclass it if it came from a third-party library that sealed it. But any subclass of HttpRequestMessage will have the property bag.

@bitm0de
Copy link

bitm0de commented Jan 28, 2021

Why not just subclass the derived class at that point then?

That couples the timeout functionality to the other property that the subclass implemented, which may not be desirable. Alternatively, I may not even be able to subclass it if it came from a third-party library that sealed it. But any subclass of HttpRequestMessage will have the property bag.

Yes, but if you’re debating use cases here then there’s also a case to be made for the mutability of the data in that dictionary being undesirable as well. If that was the case with a third party lib then it might also be the case that they hide that property to avoid external fiddling as well anyways. Thus, third party libs can introduce problems with your ability to access and use the internal dictionary just as equally as it can affect your ability to inherit from a class... There’s no guarantee that you’d have access to these things with a third party lib depending on the functionality it provides (i.e. Open-closed principle might hide this property but expose other things for extensibility). As such, I think this discussion should omit the unknown.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment