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