Better timeout handling with HttpClient

The problem

If you often use HttpClient to call REST APIs or to transfer files, you may have been annoyed by the way this class handles request timeout. There are two major issues with timeout handling in HttpClient:

  • The timeout is defined at the HttpClient level and applies to all requests made with this HttpClient; it would be more convenient to be able to specify a timeout individually for each request.
  • The exception thrown when the timeout is elapsed doesn’t let you determine the cause of the error. When a timeout occurs, you’d expect to get a TimeoutException, right? Well, surprise, it throws a TaskCanceledException! So, there’s no way to tell from the exception if the request was actually canceled, or if a timeout occurred.

Fortunately, thanks to HttpClient‘s flexibility, it’s quite easy to make up for this design flaw.

So we’re going to implement a workaround for these two issues. Let’s recap what we want:

  • the ability to specify timeout on a per-request basis
  • to receive a TimeoutException rather than a TaskCanceledException when a timeout occurs.

Specifying the timeout on a per-request basis

Let’s see how we can associate a timeout value to a request. The HttpRequestMessage class has a Properties property, which is a dictionary in which we can put whatever we need. We’re going to use this to store the timeout for a request, and to make things easier, we’ll create extension methods to access the value in a strongly-typed fashion:

public static class HttpRequestExtensions
{
    private static 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;
    }
}

Nothing fancy here, the timeout is an optional value of type TimeSpan. We can now associate a timeout value with a request, but of course, at this point there’s no code that makes use of the value…

HTTP handler

The HttpClient uses a pipeline architecture: each request is sent through a chain of handlers (of type HttpMessageHandler), and the response is passed back through these handlers in reverse order. This article explains this in greater detail if you want to know more. We’re going to insert our own handler into the pipeline, which will be in charge of handling timeouts.

Our handler is going to inherit DelegatingHandler, a type of handler designed to be chained to another handler. To implement a handler, we need to override the SendAsync method. A minimal implementation would look like this:

class TimeoutHandler : DelegatingHandler
{
    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        return await base.SendAsync(request, cancellationToken);
    }
}

The call to base.SendAsync just passes the request to the next handler. Which means that at this point, our handler does absolutely nothing useful, but we’re going to augment it gradually.

Taking into account the timeout for a request

First, let’s add a DefaultTimeout property to our handler; it will be used for requests that don’t have their timeout explicitly set:

public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(100);

The default value of 100 seconds is the same as that of HttpClient.Timeout.

To actually implement the timeout, we’re going to get the timeout value for the request (or DefaultTimeout if none is defined), create a CancellationToken that will be canceled after the timeout duration, and pass this CancellationToken to the next handler: this way, the request will be canceled after the timout is elapsed (this is actually what HttpClient does internally, except that it uses the same timeout for all requests).

To create a CancellationToken whose cancellation we can control, we need a CancellationTokenSource, which we’re going to create based on the request’s timeout:

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;
    }
}

Two points of interest here:

  • If the request’s timeout is infinite, we don’t create a CancellationTokenSource; it would never be canceled, so we save a useless allocation.
  • If not, we create a CancellationTokenSource that will be canceled after the timeout is elapsed (CancelAfter). Note that this CTS is linked to the CancellationToken we receive as a parameter in SendAsync: this way, it will be canceled either when the timeout expires, or when the CancellationToken parameter will itself be canceled. You can get more details on linked cancellation tokens in this article.

Finally, let’s change the SendAsync method to use the CancellationTokenSource we created:

protected async override Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request,
    CancellationToken cancellationToken)
{
    using (var cts = GetCancellationTokenSource(request, cancellationToken))
    {
        return await base.SendAsync(
            request,
            cts?.Token ?? cancellationToken);
    }
}

We get the CTS and pass its token to base.SendAsync. Note that we use cts?.Token, because GetCancellationTokenSource can return null; if that happens, we use the cancellationToken parameter directly.

At this point, we have a handler that lets us specify a different timeout for each request. But we still get a TaskCanceledException when a timeout occurs… Well, this is going to be easy to fix!

Throwing the correct exception

All we need to do is catch the TaskCanceledException (or rather its base class, OperationCanceledException), and check if the cancellationToken parameter is canceled: if it is, the cancellation was caused by the caller, so we let it bubble up normally; if not, this means the cancellation was caused by the timeout, so we throw a TimeoutException. Here’s the final SendAsync method:

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();
        }
    }
}

Note that we use an exception filter : this way we don’t actually catch the OperationException when we want to let it propagate, and we avoid unnecessarily unwinding the stack.

Our handler is done, now let’s see how to use it.

Using the handler

When creating an HttpClient, it’s possible to specify the first handler of the pipeline. If none is specified, an HttpClientHandler is used; this handler sends requests directly to the network. To use our new TimeoutHandler, we’re going to create it, attach an HttpClientHandler as its next handler, and pass it to the HttpClient:

var handler = new TimeoutHandler
{
    InnerHandler = new HttpClientHandler()
};

using (var client = new HttpClient(handler))
{
    client.Timeout = Timeout.InfiniteTimeSpan;
    ...
}

Note that we need to disable the HttpClient‘s timeout by setting it to an infinite value, otherwise the default behavior will interfere with our handler.

Now let’s try to send a request with a timeout of 5 seconds to a server that takes to long to respond:

var request = new HttpRequestMessage(HttpMethod.Get, "http://foo/");
request.SetTimeout(TimeSpan.FromSeconds(5));
var response = await client.SendAsync(request);

If the server doesn’t respond within 5 seconds, we get a TimeoutException instead of a TaskCanceledException, so things seem to be working as expected.

Let’s now check that cancellation still works correctly. To do this, we pass a CancellationToken that will be cancelled after 2 seconds (i.e. before the timeout expires):

var request = new HttpRequestMessage(HttpMethod.Get, "http://foo/");
request.SetTimeout(TimeSpan.FromSeconds(5));
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
var response = await client.SendAsync(request, cts.Token);

This time, we receive a TaskCanceledException, as expected.

By implementing our own HTTP handler, we were able to solve the initial problem and have a smarter timeout handling.

The full code for this article is available here.

54 thoughts on “Better timeout handling with HttpClient”

  1. Thanks for these interesting insights.
    I think your implementation of `SendAsync` is broken when you say `using (var cts = GetCancellationTokenSource(request, cancellationToken))` because `GetCancellationTokenSource` might return `null`, right? So if you still want to avoid the allocation of a `CancellationTokenSource` I would prefer a try/finally block where in the finally block you dispose `cts` only when it’s not null.

    1. Hi Johannes,

      In fact, using won’t do anything if cts is null. This code works fine:

          IDisposable x = null;
          using (x)
          {
          }
      
  2. Super-neat trick, and really useful extension to `HttpClient`!

    I have a suggestion, though (and this is a favorite personal bugbear of mine): Never ever throw exceptions without providing useful information like e.g. in this case:

    throw new TimeoutException($”Did not receive response within {request.GetTimeout() ?? DefaultTimeout} timeout”);

    would be much more fun to find in the logs some time later, when the HTTP requests in some background job are experiencing timeouts during the night, and one has started taking advantage of the ability to customize the timeout.

    1. The default message for this exception is “The operation has timed out”, which I thought was explicit enough, but you’re right, an explicit message would probably be better. Feel free to add it if you use this code 😉

  3. Hi,
    I noticed that it does not work for file downloads: looks like HttpClient prevents cancellation if download has started

    1. @max this is because when the download actually starts, the handlers in the pipeline have already been executed, so they can no longer affect how timeout is handled. The final handler receives the response as soon as the headers have been received; it doesn’t wait until the body is completely received. The actual download of the response body starts after all handlers have run (unless one of the handlers actively downloads the body).

  4. Thank you for this post! I think the issue of throwing TaskCanceledException vs TimeoutException is beyond annoying, because it makes it extremely easy to have bugs where you want to ignore real/intentional cancellations, but end up ignoring timeouts as well.

    One thing that’s worth highlighting a bit stronger is, in order for this TimeoutHandler to work properly, the timeout value on the request or TimeoutHandler.DefaultTimeout must be *shorter* than the HttpClient.Timeout. Otherwise, the HttpClient cancels its cancellation token first, and we end up with a TaskCanceledException instead of TimeoutException again.

  5. Hi Thomas,
    Great article, but this somehow doesn’t work when I use it inside a controller method of a Web API 2 project.
    Then the request is actually made, but the response never gets returned and is pending until forever.

    When I just use the HttpClient without injecting the TimeoutHandler then it work inside a Web Api controller method.
    When I use your example in a ConsoleApplication then it also works, so I think it has something to do with the Web Api runtime/pipeline.

    Do you have any idea, how to fix this?

    1. Hi Gunther, sorry for the delay, email notifications from my blog are buggy and I’m not always notified when I receive a comment…
      I don’t see any reason why it wouldn’t work. Could you show the code where you create the HttpClient?

      1. Hi Thomas,
        Sorry, that was my fault. Just found out, that I was not async all the way.
        There was a switch to synchronous code inside the workflow, that caused this deadlock.

  6. what this mean:
    The timeout is defined at the HttpClient level and applies to all requests made with this HttpClient

    Is it mean,httpClient timeout in one request, and the other request will timeout ?

  7. I new to async methods, can you please give me more details on “Using the handler” part? I am not able to understand where to use:
    var handler = new TimeoutHandler
    {
    InnerHandler = new HttpClientHandler()
    };

    using (var client = new HttpClient(handler))
    {
    client.Timeout = Timeout.InfiniteTimeSpan;

    }

    1. @nived what do you mean? This has nothing to do with async methods. You just use this where you would use new HttpClient().

    1. Because, as I said in the article, this doesn’t allow to distinguish between cancellation and timeout. Modeling timeout as a cancellation is wrong, because cancellation and timeout are very different in nature.

  8. Hi, cool article, thanks for this.
    I’ve got one comment. The following code is problematic:
    using (var client = new HttpClient(handler))
    {
    client.Timeout = Timeout.InfiniteTimeSpan;

    }
    One should NOT create an instance of HttpClient on a per-request basis. The HttpClient object should be instantiated once per domain per app lifetime. Otherwise you may break your TCP pool, as ports will remain open after the request is actually finished.

    1. Hi Marek,
      It’s true, however I wanted to keep things simple for the scope of this article. In real-life you would use HttpClientFactory or just keep a singleton instance of the client.

  9. Good stuff. Quick note:
    The piece of code above showing the default cancellationToken is returning finalCancellationToken instead of cancellationToken.

  10. I think you should catch TaskCanceledException instead of OperationCancelledException or am i missing something?

    1. @Rahul TaskCanceledException inherits from OperationCanceledException. TaskCanceledException is just more specific. Which one exactly is thrown is an implementation detail and might change in the future, so it’s safer to catch the most general type.

    1. Hi Benny,
      Can you elaborate? What do you mean exactly? This solution applies to any type of request.

    1. Hi Ritesh,
      What is “PostSync”? If you mean “PostAsync”: don’t use it, use SendAsync instead. PostAsync is just a shortcut for SendAsync(new HttpRequestMessage(HttpMethod.Post, ...))

  11. Sorry I wrote it by mistake. I was talking about PostAsync only. One more thing I want to ask I dont want asynchronous calling to Webapi. I want to wait for the response. We have to do some changes in this then?

    1. Well, HttpClient is designed to be used asynchronously. You can use `.Result` on the task to block until it completes, but in some contexts it can create a deadlock (e.g. if you’re doing this from the UI thread in a desktop or mobile app)

      1. Ya I thought so, currently I am doing that only. I am also using HttpClient factory and then using CreateClient method to create HttpClient object. And it dont HttpHandler as a parameter?

        1. You specify the HttpHandler when you declare the HttpClient with AddHttpClient:

          services.AddHttpClient(…)
          .AddHttpMessageHandler()

      2. I used .Result in the new handler like :
        protected async override Task SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
        {
        return base.SendAsync(request, cancellationToken).Result;
        }

        But then again I am getting TaskCancelled Exception but not the new Timeout exception?

  12. This is a brilliant solution, do you know if there is away to apply this to all clients created via the IHttpclientfactory, i only see a way of adding handlers to individualy named clients >.<.

    1. Hi James,

      Yes, there is! Took me a while to find it, though… Here it is:

      	services.ConfigureAll<HttpClientFactoryOptions>(options =>
      	{
      		options.HttpMessageHandlerBuilderActions.Add(builder =>
      		{
      			builder.AdditionalHandlers.Add(new TimeoutHandler());
      		});
      		options.HttpClientActions.Add(client => 
      		{
      			client.Timeout = Timeout.InfiniteTimeSpan;
      		});
      	});
      

      There’s also another way to add a handler to all clients, by registering a custom IHttpMessageHandlerBuilderFilter. The default HttpClientFactory does this to add a logging handler.

      1. Thanks for the reply, i see you can also use options.defaultname when adding it to the service as the default create client is a named client.

        Also one quick question, you add:
        `client.Timeout = Timeout.InfiniteTimeSpan;`
        i have also set this in my startup for clients.

        Doesn’t this mean that if you don’t specify a timeout for request it will always be infinite and never create a cancellation token because of your if statement, it only uses the default timeout you have specified if it cannot find a timeout but this has been set to Timeout.InfiniteTimeSpan? or am i overlooking something.

        1. @James the TimeoutHandler class has a DefaultTimeout property (with the same default value as HttpClient.Timeout), which is used for requests that don’t have an explicit timeout. Disabling the timeout on the HttpClient is necessary for this code to work, as mentioned in the article.

  13. Hi,
    I am calling 3rd party rest API as follows. How can I apply your implementation?

    private static readonly HttpClient _httpClient = new HttpClient();

    //some code

    var response = await _httpClient.PostAsync(“https://test.com/” + url, content);

    1. You need to pass the TimeoutHandler to the HttpClient constructor (see the “Using the handler” section in the article)

  14. how would you get around port exhaustion in this scenario if you constantly have to create a new httpclient instance?

  15. I am using the below code, here it is hitting the time-out exception when the code is executed from my client machine but if the same code is used in the server machine the “e.CancellationToken.IsCancellationRequested” value is not getting set.
    I wonder what I might be doing wrong.
    Can you please help?
    I would like to catch the time-out exception and use the internal code for further operations.

    try
    {
        HttpClient client = new HttpClient();
        var request = new HttpRequestMessage(methodType, url);
        client.Timeout = new TimeSpan(0, 0, (int)cmdRequestTimeout);
        return await client.SendAsync(request);
    }
    catch(TaskCanceledException e)
    {
        //code
        if (!e.CancellationToken.IsCancellationRequested)
        {
             //time-out log code
        }
    }
    
    1. Hi Vineeth, I’m not sure what you’re trying to do here… The exception’s cancellation token should always be true, otherwise the token wouldn’t be associated with a TaskCancelledException.

      1. I think Vineeth’s solution works and is cleaner. To determine whether the TaskCanceledException is due to a timeout or a real cancellation, just check the original cancellation token state.

        try
        {
        HttpRequestMessage req = new HttpRequestMessage(method, Url);
        HttpResponseMessage resp = await client.SendAsync(req, canceltoken);
        ….
        ….
        }
        catch (TaskCanceledException)
        {
        if (!canceltoken.IsCancellationRequested)
        {
        // timed out
        }
        else
        {
        // canceled through token
        }
        }

        1. Hi Andrew,

          Cleaner than what? According to Vineeth’s comment, his code doesn’t work…
          And what you propose is basically what I’m doing in the article 😉
          (except that I encapsulated it in a custom handler)

  16. Quick question: let’s say you’re developing project A, which depends on a third-party project B, but it’s project B the one which uses HttpClient instead of project A. Then in this case, your workaround cannot be used, right? (Unless I make a PR to project B.)
    Thanks

    1. @Andres, yes, you need control over the HttpClient. Many library let you provide your own HttpClient, in which case you can use this workaround. If it’s not the case here, project B needs to be modified.

Leave a Reply

Your email address will not be published. Required fields are marked *