-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TaskCanceledException.CancellationToken thrown by HttpClient does not equal to the original CancellationToken passed in #52750
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
Tagging subscribers to this area: @dotnet/ncl Issue DetailsHere is the simple code to reproduce the issue: var client = new HttpClient();
var cts = new CancellationTokenSource(1000);
var token = cts.Token;
try
{
//await client.GetAsync("https://localhost:5001/weatherforecast", token);
await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://localhost:5001/weatherforecast"), token);
}
catch (OperationCanceledException ex)
{
Console.WriteLine($"Same cancellation token: {token == ex.CancellationToken}");
} And the output Our code base has a lot of places to check if a OperationCanceledException is thrown because of passed in cancellationToken, e.g., public static bool IsOperationCanceledException(this Exception ex, CancellationToken cancellationToken) => ex is OperationCanceledException ocex && ocex.CancellationToken == cancellationToken;
catch (Exception ex) when (ex.IsOperationCanceledException(cancellationToken)) And this issue will cause exception handling issue for us.
|
@yantang-msft I believe this is by design. HttpClient.SendAsync links the token into another CTS: runtime/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs Lines 170 to 175 in 01b7e73
runtime/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs Lines 769 to 796 in 01b7e73
Did you consider just checking Looking at the
runtime/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/TaskAwaiter.cs Line 173 in 01b7e73
It doesn't seem the code wants to care if the CT was wrapping other CTs and which one is the original source (it would have hard time to pick one when there are multiple in the tree). |
Is this how linked CTS's are intended to work? I would naively expect that using a linked CTS would still result in a TaskCanceledExecption that specifies which specific CTS was cancelled, not the linked one. @stephentoub thoughts? |
@karelz Thanks for looking into it, the LinkedCancellationTokenSource is what I thought. |
That doesn't compose well. For example, consider code like: void A(CancellationToken ct)
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(ct))
B(cts.Token);
}
void B(CancellationToken ct)
{
try
{
C(ct);
}
catch (OperationCanceledException oce) when (oce.CancellationToken == ct)
{
...
}
} If method C does what you suggest and throws an exception with one of the underlying exceptions rather than the token passed in to C, B's behavior will break. If you have a method like: void M(CancellationToken cancellationToken)
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
Foo(cts.Token);
} and its a goal to ensure that any cancellation exceptions that emerge from M due to cancellationToken having cancellation requested contain that passed in token, method M needs to do that exception management... it's the only code that has all the necessary context to know the intended behavior. void M(CancellationToken cancellationToken)
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
try
{
Foo(cts.Token);
}
catch (OperationCanceledException oce) when (oce.CancellationToken == cts.Token)
{
throw new OperationCanceledException(oce.Message, oce, cancellationToken);
}
}
} HttpClient.HandleFailure can be updated accordingly. |
Makes sense. It does seem like the implication here is: Every method that uses CreateLinkedTokenSource to add additional cancellation conditions on top of an existing CancellationToken probably needs to have the above logic to ensure that the proper CancellationToken is propagated in the exception. Correct? I have a feeling we have places that are not handling this correctly. |
If said method is exposed publicly from a library where a caller may have expectations around the token included in the exception, yes. |
Even if it's not exposed publicly, some internal logic may have expectations around the token in the exception. And even if it doesn't today, the code could evolve so that it does in the future. It seems reasonable to say that a method that takes a CancellationToken should always report that CancellationToken in the TaskCanceledException if that CancellationToken is the cause of cancellation. I suppose this could be avoided if we know it's never ever going to be needed, but that seems like the exception (lol) and not the rule. |
In my experience, for internal code it's the minority case that the consumer cares about special-casing the specific token. |
Here is the simple code to reproduce the issue:
And the output
Same cancellation token: False
Our code base has a lot of places to check if a OperationCanceledException is thrown because of passed in cancellationToken, e.g.,
And this issue will cause exception handling issue for us.
The text was updated successfully, but these errors were encountered: