简体   繁体   中英

Passing in an already cancelled CancellationToken causes HttpClient to hang

I am wanting to use a CancellationToken to cancel a call to HttpClient.PostAsJsonAsync . However, with the following setup the call to PostAsJsonAsync hangs indefinitely (I left it running for several minutes).

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

Note that I am passing an already cancelled CancellationTokenSource - I have the same problem if I cancel the token using Task.Delay with a short delay.

I realise I could simply check whether the token has been cancelled prior to the call, but even so, I have the same problem if the token gets cancelled after a short delay, ie, it's not cancelled before the method call but becomes so shortly after it.

So my question is, what is causing this and what can I do to work around/fix it?

Edit

For those looking for a workaround, inspired by @Darrel Miller's answer, I came up with the following extension method:

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

It definitely seems to be a bug that you hit You can work around it by constructing the HttpContent/ObjectContent object yourself, like this.

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

Calling the content.LoadIntoBufferAsync forces the deserialization to happen before the PostAsync and seems to avoid the deadlock.

Agree with @Darrel Miller's answer. This is a bug. Just adding more detail for the bug report.

The problem is that internally a TaskCompletionSource is used, but when an exception is thrown due to the cancellation in this specific case, it is not caught, and the TaskCompletionSource is never set into one of the completed states (and thus, waiting on the TaskCompletionSource 's Task will never return.

Using ILSpy, looking at HttpClientHandler.SendAsync you can see the 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;
}

Later on, through the line Task.Factory.StartNew(this.startRequest, requestState); we get to the following method:

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

You'll notice that the delegate in the call to ContinueWithStandard has no exception handling within the delegate, and no one holds on to the returned task (and thus when this task throws an exception, it is ignored). The call to this.StartGettingRequestStream(state); does throw an exception:

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: 

Here's the full callstack at the time of the exception:

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

I believe the intention is to not ignore it, and in the case of an exception call the HandleAsyncException method which does set the TaskCompletionSource into a final state.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM