Search code examples
c#task-parallel-libraryscalabilitydotnet-httpclientcancellation

Canceling an async call on a static HttpClient


I am using a static HttpClient (for scalability reasons - see What is the overhead of creating a new HttpClient per call in a WebAPI client?) and would like to be able to cancel individual requests that take too long. There is an overload on SendAsync that takes a CancellationToken - but I don't know if it's thread-safe since my HttpClient instance is static. For example, if I have several requests being sent thru the HttpClient simultaneously and I try to cancel one, does it cancel the right one?

I looked thru the HttpClient code and at first glance it doesn’t look like it is thread-safe to do so since the cancellation is sent to the HttpClientHandler (which is the same for all requests). But I could be missing something. So my questions are:

  1. Can I cancel individual requests on a static HttpClient?
  2. If not, how can I accomplish this?

NOTE: Since testing this, requires a way to reliably create a race condition, in code that I do not control, I don't see a way to test this.


Solution

  • Each SendAsync call is totally independent from each other, canceling the token for one request does not cancel other outstanding requests.

    Your assumption that because HttpClientHandler is shared for all requests that means all requests get canceled is incorrect. If you look in to the decompiled source of HttpClientHandler you will see

    [__DynamicallyInvokable]
    protected internal override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
      if (request == null)
        throw new ArgumentNullException(nameof (request), SR.net_http_handler_norequest);
      this.CheckDisposed();
      if (Logging.On)
        Logging.Enter(Logging.Http, (object) this, nameof (SendAsync), (object) request);
      this.SetOperationStarted();
      TaskCompletionSource<HttpResponseMessage> completionSource = new TaskCompletionSource<HttpResponseMessage>();
      HttpClientHandler.RequestState state = new HttpClientHandler.RequestState();
      state.tcs = completionSource;
      state.cancellationToken = cancellationToken;
      state.requestMessage = request;
      try
      {
        HttpWebRequest prepareWebRequest = this.CreateAndPrepareWebRequest(request);
        state.webRequest = prepareWebRequest;
        cancellationToken.Register(HttpClientHandler.onCancel, (object) prepareWebRequest);
        if (ExecutionContext.IsFlowSuppressed())
        {
          IWebProxy webProxy = (IWebProxy) null;
          if (this.useProxy)
            webProxy = this.proxy ?? WebRequest.DefaultWebProxy;
          if (this.UseDefaultCredentials || this.Credentials != null || webProxy != null && webProxy.Credentials != null)
            this.SafeCaptureIdenity(state);
        }
        Task.Factory.StartNew(this.startRequest, (object) state);
      }
      catch (Exception ex)
      {
        this.HandleAsyncException(state, ex);
      }
      if (Logging.On)
        Logging.Exit(Logging.Http, (object) this, nameof (SendAsync), (object) completionSource.Task);
      return completionSource.Task;
    }
    

    The cancellation token is getting wrapped up in a new HttpClientHandler.RequestState state object every call of SendAsnyc, when the token is canceled only the state.webRequest associated with that state object is the one that will be canceled.