web-dev-qa-db-fra.com

Annulation d'une demande HttpClient - Pourquoi TaskCanceledException.CancellationToken.IsCancellationRequested false?

Étant donné le code suivant:

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

Je m'attendrais à ex.CancellationToken.IsCancellationRequested être true à l'intérieur du bloc catch, mais ce n'est pas le cas. Suis-je en train de mal comprendre quelque chose?

30
Todd Menier

C'est le cas parce que HttpClient en interne (dans SendAsync) utilise un TaskCompletionSource pour représenter l'opération async. Il renvoie TaskCompletionSource.Task Et c'est la tâche sur laquelle vous await.

Il appelle ensuite base.SendAsync Et enregistre une continuation sur la tâche retournée qui annule/termine/échoue la tâche de TaskCompletionSource en conséquence.

Dans le cas d'une annulation, il utilise TaskCompletionSource.TrySetCanceled qui associe la tâche annulée à un nouveau CancellationToken (default(CancellationToken)).

Vous pouvez le voir en regardant le TaskCanceledException. En plus de ex.CancellationToken.IsCancellationRequested Étant falseex.CancellationToken.CanBeCanceled Est également false, ce qui signifie que ce CancellationToken ne peut jamais être annulé car il n'a pas été créé en utilisant un CancellationTokenSource.


OMI, il devrait utiliser TaskCompletionSource.TrySetCanceled(CancellationToken) à la place. De cette façon, le TaskCompletionSource sera associé au CancellationToken transmis par le consommateur et pas simplement le CancellationToken par défaut. Je pense que c'est un bug (bien que mineur) et j'ai soumis un problème lors de la connexion à ce sujet.

43
i3arnon

@Bengie Cela n'a pas fonctionné pour moi. J'ai dû le modifier un peu. IsCancellationRequested a toujours renvoyé true, donc je ne pouvais pas compter sur cela.

Cela a fonctionné pour moi:

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;
1
Janett Holst

J'ai défini le délai d'infini pour le désactiver, puis je passe mon propre jeton d'annulation.

using(CancellationTokenSource cancelAfterDelay = new CancellationTokenSource(timespan/timeout))
...
catch(OperationCanceledException e)
{
if(!cancelAfterDelay.Token.IsCancellationRequested)
throw new TimeoutException($"An HTTP request to {request.Uri} timed out ({(int)requestTimeout.TotalSeconds} seconds)");
else
throw;
}
0
Bengie