28

HttpClient has a builtin timeout feature (despite being all asynchronous, i.e. timeouts could be considered orthogonal to the http request functionality and thus be handled by generic asynchronous utilities, but that aside) and when the timeout kicks in, it'll throw a TaskCanceledException (wrapped in an AggregateException).

The TCE contains a CancellationToken that equals CancellationToken.None.

Now if I provide HttpClient with a CancellationToken of my own and use that to cancel the operation before it finishes (or times out), I get the exact same TaskCanceledException, again with a CancellationToken.None.

Is there still a way, by looking only at the exception thrown, to figure out whether a timeout canceled the request, without having to make my own CancellationToken accessible to the code that checks the exception?

P.S. Could this be a bug and CancellationToken got somehow wrongly fixed to CancellationToken.None? In the cancelled using custom CancellationToken case, I'd expect TaskCanceledException.CancellationToken to equal that custom token.

Edit To make the problem a bit more clear, with access to the original CancellationTokenSource, it is easy to distinguish timeout and user cancellation:

origCancellationTokenSource.IsCancellationRequested == true

Getting the CancellationToken from the exception though gives the wrong answer:

((TaskCanceledException) e.InnerException).CancellationToken.IsCancellationRequested == false

Here a minimal example, due to popular demand:

public void foo()
{
    makeRequest().ContinueWith(task =>
    {
        try
        {
            var result = task.Result;
            // do something with the result;
        }
        catch (Exception e)
        {
            TaskCanceledException innerException = e.InnerException as TaskCanceledException;
            bool timedOut = innerException != null && innerException.CancellationToken.IsCancellationRequested == false;

            // Unfortunately, the above .IsCancellationRequested
            // is always false, no matter if the request was
            // cancelled using CancellationTaskSource.Cancel()
            // or if it timed out
        }
    });
}

public Task<HttpResponseMessage> makeRequest()
{
    var cts = new CancellationTokenSource();
    HttpClient client = new HttpClient() { Timeout = TimeSpan.FromSeconds(10) };
    HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "url");

    passCancellationTokenToOtherPartOfTheCode(cts);
    return client.SendAsync(httpRequestMessage, cts.Token);
}
Evgeniy Berezovsky
  • 18,571
  • 13
  • 82
  • 156
  • Please post a minimal piece of code that exhibits this behaviour. – Richard Cook Mar 01 '13 at 06:26
  • why isn't the code that starts the task handling the exception? That piece of code should already have the CancellationToken, it could manage that scenario and then just throw the exception that you want your higher level try/catch block to receive, without the TokenSource – Francisco Noriega Mar 01 '13 at 23:29
  • 1
    @FranciscoNoriega Sometimes you want to separate code, e.g. to make it reusable, and also in asynchronously executed code. So you end up catching exceptions in different places than where the code executed is defined. I don't want to have to pass all state that is involved around, especially as the exception seems to provide that state when it is needed - except it does not work like I'd think it's supposed to. Have a look at the stripped down example that roughly shows how I use it. – Evgeniy Berezovsky Mar 05 '13 at 01:33
  • 1
    @EugeneBeresovksy, have you managed to workaround the problem? I'm in a similar situation, I have an app where the most of exception handling is done in one place. – Arthur Nunes Nov 28 '13 at 21:22
  • @ArthurNunes What you could do is create your own `MyTaskCanceledException` with a flag. You then catch the original `TaskCanceledException` in a scope where you have access to the original `CancellationTokenSource` and repackage it into your own exception, setting the flag using `origCancellationTokenSource.IsCancellationRequested`. – Evgeniy Berezovsky Nov 29 '13 at 00:47
  • @EugeneBeresovksy, in the end I did that, but I throw TimeoutException instead. The problem with that approach is that I have to do this every time I use HttpClient, or create my own extension methods. – Arthur Nunes Nov 29 '13 at 15:25

2 Answers2

6

The accepted answer is certainly how this should work in theory, but unfortunately in practice IsCancellationRequested does not (reliably) get set on the token that is attached to the exception:

Cancelling an HttpClient Request - Why is TaskCanceledException.CancellationToken.IsCancellationRequested false?

Community
  • 1
  • 1
Todd Menier
  • 37,557
  • 17
  • 150
  • 173
  • you may not be able to use the attached token, but if you have the original token (that was passed to `SendAsync`), you can use that. Or am I missing something? – Josef Bláha Sep 29 '16 at 07:00
  • I see, I'm missing that the question required relying only on the exception data. – Josef Bláha Sep 29 '16 at 08:46
4

Yes, they both return the same exception (possibly because of timeout internally using a token too) but it can be easily figured out by doing this:

   catch (OperationCanceledException ex)
            {
                if (token.IsCancellationRequested)
                {
                    return -1;
                }

                return -2;
            }

so basically if you hit the exception but your token is not cancelled, well it was a regular http timeout

Francisco Noriega
  • 13,725
  • 11
  • 47
  • 72
  • I made **by looking only at the exception thrown** in my question bold and added a bit more specifics to make it more clear what I'm asking. – Evgeniy Berezovsky Mar 01 '13 at 06:12
  • `OperationCanceledException` is also thrown if the `HttpClient` is disposed of while the request is pending, or if `HttpClient.CancelPendingRequests` is called. These cases can be distinguished if you control the calls to these methods. – Ernesto Jan 10 '15 at 01:40
  • this answer lacks details... where does `token` come from? is it a property of the exception? downvoting until you improve it – ympostor Jan 12 '17 at 04:30
  • @ympostor that information is there in the original question. He is providing his own token, that's where it comes from. – Francisco Noriega Jan 12 '17 at 06:13