8

Desidero utilizzare uno CancellationToken per annullare una chiamata a HttpClient.PostAsJsonAsync. Tuttavia, con la configurazione seguente la chiamata a PostAsJsonAsync si blocca indefinitamente (l'ho lasciata in esecuzione per alcuni minuti).L'inoltro di un annullamentoToken già annullato causa l'interruzione di HttpClient

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

Si noti che sto passando un già annullato CancellationTokenSource - Ho lo stesso problema se annullo il token usando Task.Delay con un breve ritardo.

Mi rendo conto che potrei semplicemente verificare se il token è stato cancellato prima della chiamata, ma anche così, ho lo stesso problema se il token viene cancellato dopo un breve ritardo, cioè, non è cancellato prima della chiamata al metodo ma diventa così poco dopo.

Quindi la mia domanda è, cosa sta causando questo e cosa posso fare per aggirare/risolvere il problema?

Modifica

Per chi cerca una soluzione, ispirata dalla risposta di @Darrel Miller, mi si avvicinò con il seguente metodo di estensione:

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

Potrebbe essersi verificato un deadlock per il blocco del thread principale, si blocca se si prova su un'applicazione di console o se si esegue 'attende l'attività 'invece di' task.Wait'? –

+0

@ScottChamberlain La stessa cosa accade con entrambi i task 'await;' e 'task.Wait'. –

+0

@nick_w, qual è l'ambiente di esecuzione per questo (un'applicazione per l'interfaccia utente desktop, ASP.NET, console, ecc.)? Inoltre, usi 'token.Register 'ovunque? – Noseratio

risposta

7

Sembra decisamente essere un bug che si hit Puoi aggirare il problema costruendo tu stesso l'oggetto HttpContent/ObjectContent, in questo modo.

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

Chiamando il content.LoadIntoBufferAsync forza la deserializzazione per accadere prima che il PostAsync e sembra evitare la situazione di stallo.

+0

+1 Ben fatto. Ho incorporato questo in un metodo di estensione come mostrato nella mia domanda aggiornata. –

+0

@nick_w, dovresti considerare di usare Attendere invece di Aspettare nel metodo di estensione –

+0

@MattSmith Buon punto, aggiornato –

6

D'accordo con la risposta di @Darrel Miller. Questo è un bug Aggiungendo solo più dettagli per il bug report.

Il problema è che internamente un TaskCompletionSource viene utilizzato, ma quando viene generata un'eccezione causa della cancellazione in questo caso specifico, esso non si trova, e la TaskCompletionSource non è impostato in uno degli stati completati (e quindi, . in attesa di 's Task il TaskCompletionSource non tornerà mai

Utilizzando ILSpy, guardando HttpClientHandler.SendAsync potete vedere la 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; 
} 

in seguito, attraverso la linea Task.Factory.StartNew(this.startRequest, requestState); otteniamo la seguente Metodo:

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

Si noterà che il delegato nella chiamata a ContinueWithStandard non ha la gestione delle eccezioni all'interno del delegato, e nessuno tiene al compito tornato (e quindi quando questa operazione genera un'eccezione, è ignorato). La chiamata a this.StartGettingRequestStream(state); fa un'eccezione:

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: 

Ecco il completo stack di chiamate al momento dell'eccezione:

> 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] 

Credo che l'intenzione è quella di non ignorarlo, e nel caso di un l'eccezione chiama il metodo HandleAsyncException che imposta lo TaskCompletionSource in uno stato finale.

+0

Un buon lavoro di scavo, quindi è System.Net.Http che presenta il problema e non la libreria del client WebApi come suggerito. Mi chiedo se questo significhi che se il Il formatter genera un'eccezione durante il tentativo di serializzare il corpo della richiesta, si verifica lo stesso problema di impiccagione –

+0

+1 Questo è un buon codice speleologico. Nel mio debugging, ho notato che il 'System.Net.WebException' che viene lanciato è in fatto colto, anche se penso che tu abbia ragione in quanto 'TaskCompletionSource' non viene impostato correttamente. –

+0

@DarrelMiller Sospetto che tu possa essere coinvolto in qualcosa lì. Usando Fiddler, ho scoperto che non c'era traffico inviato quando si passa un token annullato (come ci si aspetterebbe). Quando si annulla il token in un secondo momento, il traffico è stato inviato ma la richiesta è stata annullata come previsto. –