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

26,000

Solution 1

That's the case because HttpClient internally (in SendAsync) is using a TaskCompletionSource to represent the async operation. It returns TaskCompletionSource.Task and that's the task you await on.

It then calls base.SendAsync and registers a continuation on the returned task that cancels/completes/faults the TaskCompletionSource's task accordingly.

In the case of cancellation it uses TaskCompletionSource.TrySetCanceled which associates the canceled task with a new CancellationToken (default(CancellationToken)).

You can see that by looking at the TaskCanceledException. On top of ex.CancellationToken.IsCancellationRequested being false ex.CancellationToken.CanBeCanceled is also false, meaning that this CancellationToken can never be canceled as it wasn't created using a CancellationTokenSource.


IMO it should be using TaskCompletionSource.TrySetCanceled(CancellationToken) instead. That way the TaskCompletionSource will be associated with the CancellationToken passed in by the consumer and not simply the default CancellationToken. I think it's a bug (though a minor one) and I submitted an issue on connect about it.

Solution 2

@Bengie This didn't work for me. I had to alter it a little. IsCancellationRequested always returned true so i couldn't rely on that.

This worked for me:

using (CancellationTokenSource cancelAfterDelay = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)))
{
    DateTime startedTime = DateTime.Now;

    try
    {
        response = await request.ExecuteAsync(cancelAfterDelay.Token);
    }
    catch (TaskCanceledException e)
    {
        DateTime cancelledTime = DateTime.Now;
        if (startedTime.AddSeconds(timeout-1) <= cancelledTime)
        {
            throw new TimeoutException($"An HTTP request to {request.Url} timed out ({timeout} seconds)");
        }
        else
            throw;
    }
}
return response;
Share:
26,000
Todd Menier
Author by

Todd Menier

OSS projects: Flurl - Fluent URL builder and wrist-friendly HTTP client library for .NET. AsyncPoco - Fully asynchronous fork of PetaPoco micro-ORM for .NET.

Updated on June 03, 2020

Comments

  • Todd Menier
    Todd Menier almost 4 years

    Given the following code:

    var cts = new CancellationTokenSource();
    
    try 
    {
        // get a "hot" task
        var task = new HttpClient().GetAsync("http://www.google.com", cts.Token);
    
        // request cancellation
        cts.Cancel();
    
        await task;
    
        // pass:
        Assert.Fail("expected TaskCanceledException to be thrown");
    }
    catch (TaskCanceledException ex) 
    {
        // pass:
        Assert.IsTrue(cts.Token.IsCancellationRequested,
            "expected cancellation requested on original token");
    
        // fail:
        Assert.IsTrue(ex.CancellationToken.IsCancellationRequested,
            "expected cancellation requested on token attached to exception");
    }
    

    I would expect ex.CancellationToken.IsCancellationRequested to be true inside the catch block, but it is not. Am I misunderstanding something?