8

Ich möchte eine CancellationToken verwenden, um einen Anruf zu HttpClient.PostAsJsonAsync abzubrechen. Bei dem folgenden Setup bleibt der Anruf auf PostAsJsonAsync jedoch unbegrenzt (ich habe ihn einige Minuten laufen lassen).Das Übergeben eines bereits abgebrochenen CancellationToken führt dazu, dass HttpClient hängt

CancellationTokenSource source = new CancellationTokenSource(); 
source.Cancel(); 
HttpClient client = new HttpClient(); 

try 
{ 
    var task = client.PostAsJsonAsync<MyObject>("http://server-address.com", 
     new MyObject(), source.Token); 

    task.Wait(); 
} 
catch (Exception ex) 
{ 
    //Never gets hit. 
} 

Bitte beachte, dass ich vorbei bin ein bereits abgesagt CancellationTokenSource - ich habe das gleiche Problem, wenn ich das Token Task.Delay mit einer kurzen Verzögerung abzubrechen.

Ich weiß, ich könnte einfach überprüfen, ob das Token vor dem Anruf abgebrochen wurde, aber trotzdem habe ich das gleiche Problem, wenn das Token nach einer kurzen Verzögerung abgebrochen wird, dh es nicht vor dem Methodenaufruf abgebrochen wird wird so kurz danach.

Also meine Frage ist, was verursacht dies und was kann ich tun, um es zu umgehen/zu beheben?

bearbeiten

für die Suche nach einer Vermeidung des Problems suchen, inspiriert von @Darrel Millers Antwort, kam ich auf die folgenden Erweiterungsmethode auf:

public static async Task<HttpResponseMessage> PostAsJsonAsync2<T>(this HttpClient client, string requestUri, T value, CancellationToken token) 
{ 
    var content = new ObjectContent(typeof(T), value, new JsonMediaTypeFormatter()); 
    await content.LoadIntoBufferAsync(); 

    return await client.PostAsync(requestUri, content, token); 
} 
+0

Sie können einen Deadlock für das Blockieren des Hauptthreads erhalten, hängt es, wenn Sie dies in einer Konsolenanwendung versuchen, oder wenn Sie 'task warten;' statt 'task.Wait'? –

+0

@ScottChamberla Die gleiche Sache passiert mit "warten Aufgabe" und "task.Wait". –

+0

@nick_w, was ist die Ausführungsumgebung dafür (eine Desktop-UI-Anwendung, ASP.NET, Konsole usw.)? Verwenden Sie auch 'token.Register' irgendwo? – Noseratio

Antwort

7

Es scheint auf jeden Fall ein Fehler zu sein, dass Sie hit Sie können umgehen, indem Sie das HttpContent/ObjectContent-Objekt selbst konstruieren.

CancellationTokenSource source = new CancellationTokenSource(); 
source.Cancel(); 
HttpClient client = new HttpClient(); 

var content = new ObjectContent(typeof (MyObject), new MyObject(), new JsonMediaTypeFormatter()); 
content.LoadIntoBufferAsync().Wait(); 
try 
{ 
    var task = client.PostAsync("http://server-address.com",content, source.Token); 

    task.Wait(); 
} 
catch (Exception ex) 
{ 
    //This will get hit now with an AggregateException containing a TaskCancelledException. 
} 

die content.LoadIntoBufferAsync Aufruf zwingt die Deserialisierung vor dem PostAsync passieren und scheint aus der Sackgasse zu vermeiden.

+0

+1) Schön gemacht. Ich habe dies in eine Erweiterungsmethode eingebaut, wie in meiner aktualisierten Frage gezeigt. –

+0

@nick_w, sollten Sie überlegen, statt warten .Warten Sie in Ihrer Erweiterungsmethode. –

+0

@MattSmith Guter Punkt. Aktualisiert. –

6

Stimmen Sie mit @Darrel Millers Antwort überein. Dies ist ein Fehler. Fügen Sie einfach mehr Details für den Fehlerbericht hinzu.

Das Problem ist, dass intern eine TaskCompletionSource verwendet wird, aber wenn eine Ausnahme wegen der Stornierung in diesem speziellen Fall ausgelöst wird, wird es nicht gefangen, und die TaskCompletionSource wird nie in einen der abgeschlossenen Staaten gesetzt (und damit . warten auf die TaskCompletionSource ‚s Task nie zurückkehren wird

ILSpy Verwendung bei HttpClientHandler.SendAsync suchen, können Sie sehen die TaskCompletionSource:

// System.Net.Http.HttpClientHandler 
/// <summary>Creates an instance of <see cref="T:System.Net.Http.HttpResponseMessage" /> based on the information provided in the <see cref="T:System.Net.Http.HttpRequestMessage" /> as an operation that will not block.</summary> 
/// <returns>Returns <see cref="T:System.Threading.Tasks.Task`1" />.The task object representing the asynchronous operation.</returns> 
/// <param name="request">The HTTP request message.</param> 
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param> 
/// <exception cref="T:System.ArgumentNullException">The <paramref name="request" /> was null.</exception> 
protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 
{ 
    if (request == null) 
    { 
     throw new ArgumentNullException("request", SR.net_http_handler_norequest); 
    } 
    this.CheckDisposed(); 
    if (Logging.On) 
    { 
     Logging.Enter(Logging.Http, this, "SendAsync", request); 
    } 
    this.SetOperationStarted(); 
    TaskCompletionSource<HttpResponseMessage> taskCompletionSource = new TaskCompletionSource<HttpResponseMessage>(); 
    HttpClientHandler.RequestState requestState = new HttpClientHandler.RequestState(); 
    requestState.tcs = taskCompletionSource; 
    requestState.cancellationToken = cancellationToken; 
    requestState.requestMessage = request; 
    this.lastUsedRequestUri = request.RequestUri; 
    try 
    { 
     HttpWebRequest httpWebRequest = this.CreateAndPrepareWebRequest(request); 
     requestState.webRequest = httpWebRequest; 
     cancellationToken.Register(HttpClientHandler.onCancel, httpWebRequest); 
     if (ExecutionContext.IsFlowSuppressed()) 
     { 
      IWebProxy webProxy = null; 
      if (this.useProxy) 
      { 
       webProxy = (this.proxy ?? WebRequest.DefaultWebProxy); 
      } 
      if (this.UseDefaultCredentials || this.Credentials != null || (webProxy != null && webProxy.Credentials != null)) 
      { 
       this.SafeCaptureIdenity(requestState); 
      } 
     } 
     Task.Factory.StartNew(this.startRequest, requestState); 
    } 
    catch (Exception e) 
    { 
     this.HandleAsyncException(requestState, e); 
    } 
    if (Logging.On) 
    { 
     Logging.Exit(Logging.Http, this, "SendAsync", taskCompletionSource.Task); 
    } 
    return taskCompletionSource.Task; 
} 

Später, durch die Leitung Task.Factory.StartNew(this.startRequest, requestState); wir auf die folgende erhalten Methode:

// System.Net.Http.HttpClientHandler 
private void PrepareAndStartContentUpload(HttpClientHandler.RequestState state) 
{ 
    HttpContent requestContent = state.requestMessage.Content; 
    try 
    { 
     if (state.requestMessage.Headers.TransferEncodingChunked == true) 
     { 
      state.webRequest.SendChunked = true; 
      this.StartGettingRequestStream(state); 
     } 
     else 
     { 
      long? contentLength = requestContent.Headers.ContentLength; 
      if (contentLength.HasValue) 
      { 
       state.webRequest.ContentLength = contentLength.Value; 
       this.StartGettingRequestStream(state); 
      } 
      else 
      { 
       if (this.maxRequestContentBufferSize == 0L) 
       { 
        throw new HttpRequestException(SR.net_http_handler_nocontentlength); 
       } 
       requestContent.LoadIntoBufferAsync(this.maxRequestContentBufferSize).ContinueWithStandard(delegate(Task task) 
       { 
        if (task.IsFaulted) 
        { 
         this.HandleAsyncException(state, task.Exception.GetBaseException()); 
         return; 
        } 
        contentLength = requestContent.Headers.ContentLength; 
        state.webRequest.ContentLength = contentLength.Value; 
        this.StartGettingRequestStream(state); 
       }); 
      } 
     } 
    } 
    catch (Exception e) 
    { 
     this.HandleAsyncException(state, e); 
    } 
} 

Sie werden bemerken, dass der Delegat in dem Aufruf von ContinueWithStandard innerhalb der Delegierten keine Ausnahmebehandlung hat, und niemand hält sich an die zurückgegebene Aufgabe (und damit, wenn diese Aufgabe löst eine Ausnahme, es ist ignoriert). Der Aufruf von this.StartGettingRequestStream(state); ist eine Ausnahme:

System.Net.WebException occurred 
    HResult=-2146233079 
    Message=The request was aborted: The request was canceled. 
    Source=System 
    StackTrace: 
     at System.Net.HttpWebRequest.BeginGetRequestStream(AsyncCallback callback, Object state) 
    InnerException: 

Hier ist die vollständige Aufrufliste zum Zeitpunkt der Ausnahme:

> System.dll!System.Net.HttpWebRequest.BeginGetRequestStream(System.AsyncCallback callback, object state) Line 1370 C# 
    System.Net.Http.dll!System.Net.Http.HttpClientHandler.StartGettingRequestStream(System.Net.Http.HttpClientHandler.RequestState state) + 0x82 bytes 
    System.Net.Http.dll!System.Net.Http.HttpClientHandler.PrepareAndStartContentUpload.AnonymousMethod__0(System.Threading.Tasks.Task task) + 0x92 bytes  
    mscorlib.dll!System.Threading.Tasks.ContinuationTaskFromTask.InnerInvoke() Line 59 + 0xc bytes C# 
    mscorlib.dll!System.Threading.Tasks.Task.Execute() Line 2459 + 0xb bytes C# 
    mscorlib.dll!System.Threading.Tasks.Task.ExecutionContextCallback(object obj) Line 2815 + 0x9 bytes C# 
    mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 581 + 0xd bytes C# 
    mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 530 + 0xd bytes C# 
    mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) Line 2785 C# 
    mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Line 2728 C# 
    mscorlib.dll!System.Threading.Tasks.ThreadPoolTaskScheduler.TryExecuteTaskInline(System.Threading.Tasks.Task task, bool taskWasPreviouslyQueued) Line 91 + 0xb bytes C# 
    mscorlib.dll!System.Threading.Tasks.TaskScheduler.TryRunInline(System.Threading.Tasks.Task task, bool taskWasPreviouslyQueued) Line 221 + 0x12 bytes C# 
    mscorlib.dll!System.Threading.Tasks.TaskContinuation.InlineIfPossibleOrElseQueue(System.Threading.Tasks.Task task, bool needsProtection) Line 259 + 0xe bytes C# 
    mscorlib.dll!System.Threading.Tasks.StandardTaskContinuation.Run(System.Threading.Tasks.Task completedTask, bool bCanInlineContinuationTask) Line 334 + 0xc bytes C# 
    mscorlib.dll!System.Threading.Tasks.Task.ContinueWithCore(System.Threading.Tasks.Task continuationTask, System.Threading.Tasks.TaskScheduler scheduler, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskContinuationOptions options) Line 4626 + 0x12 bytes C# 
    mscorlib.dll!System.Threading.Tasks.Task.ContinueWith(System.Action<System.Threading.Tasks.Task> continuationAction, System.Threading.Tasks.TaskScheduler scheduler, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskContinuationOptions continuationOptions, ref System.Threading.StackCrawlMark stackMark) Line 3840 C# 
    mscorlib.dll!System.Threading.Tasks.Task.ContinueWith(System.Action<System.Threading.Tasks.Task> continuationAction, System.Threading.CancellationToken cancellationToken, System.Threading.Tasks.TaskContinuationOptions continuationOptions, System.Threading.Tasks.TaskScheduler scheduler) Line 3805 + 0x1b bytes C# 
    System.Net.Http.dll!System.Net.Http.HttpUtilities.ContinueWithStandard(System.Threading.Tasks.Task task, System.Action<System.Threading.Tasks.Task> continuation) + 0x2c bytes 
    System.Net.Http.dll!System.Net.Http.HttpClientHandler.PrepareAndStartContentUpload(System.Net.Http.HttpClientHandler.RequestState state) + 0x16b bytes 
    System.Net.Http.dll!System.Net.Http.HttpClientHandler.StartRequest(object obj) + 0x5a bytes 
    mscorlib.dll!System.Threading.Tasks.Task.InnerInvoke() Line 2835 + 0xd bytes C# 
    mscorlib.dll!System.Threading.Tasks.Task.Execute() Line 2459 + 0xb bytes C# 
    mscorlib.dll!System.Threading.Tasks.Task.ExecutionContextCallback(object obj) Line 2815 + 0x9 bytes C# 
    mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 581 + 0xd bytes C# 
    mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 530 + 0xd bytes C# 
    mscorlib.dll!System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref System.Threading.Tasks.Task currentTaskSlot) Line 2785 C# 
    mscorlib.dll!System.Threading.Tasks.Task.ExecuteEntry(bool bPreventDoubleExecution) Line 2728 C# 
    mscorlib.dll!System.Threading.Tasks.Task.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Line 2664 + 0x7 bytes C# 
    mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch() Line 829 C# 
    mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Line 1170 + 0x5 bytes C# 
    [Native to Managed Transition] 

Ich glaube, die Absicht ist, es nicht zu ignorieren, und im Fall eines Ausnahme rufen Sie die HandleAsyncException Methode, die TaskCompletionSource in einen Endzustand setzt.

+0

Nice graben Job. So ist es System.Net.Http, das das Problem hat und nicht die WebApi Client-Bibliothek, wie ich vorgeschlagen habe. Ich frage mich, ob dies bedeutet, dass wenn Formatter löst beim Versuch, den Request-Body zu serialisieren, eine Exception aus, aber Sie bekommen das gleiche hängende Problem. –

+0

+1 Das ist ein gutes Code-Spelunking. In meinem eigenen Debugging habe ich bemerkt, dass die 'System.Net.WebException', die geworfen wird, drin ist Tatsache gefangen, obwohl ich denke, Sie haben Recht, dass TaskCompletionSource wird nicht richtig eingestellt. –

+0

@DarrelMiller Ich vermute, du könntest dort etwas erleben. Mit Fiddler habe ich festgestellt, dass kein Traffic gesendet wurde, wenn ein storniertes Token übergeben wurde (wie zu erwarten war). Wenn der Token etwas später storniert wurde, wurde der Verkehr gesendet, aber die Anfrage wurde wie erwartet abgebrochen. –

Verwandte Themen