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?
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.