2

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.

Dave Black
  • 7,305
  • 2
  • 52
  • 41
  • 2
    Have you considered testing it? The scenario you describe doesn't sound too complicated. – Dido Nov 10 '17 at 16:28
  • Take a look at [this](https://stackoverflow.com/questions/21786465/cancel-getasync-request-from-outside-of-method). – xRed Nov 10 '17 at 16:30
  • @Dido - Testing for a race condition is not reliable - especially since the work is all done in a library you have no control over. If you can come up with a guaranteed test for a race condition, I'm all ears. – Dave Black Nov 10 '17 at 16:31
  • @xRed - this would cancel all requests. I don't want to cancel all requests. – Dave Black Nov 10 '17 at 16:34
  • Docs don't say. If I were you, I'd spin up twenty odd tasks, making requests to goog and spin up twenty more cancelling them. Let that run overnight, if I'm that paranoid. Otherwise, it's off to the reference source with ye. –  Nov 10 '17 at 16:39
  • to my understanding of CT, you just need to pass new token for each request. Then, cancelling one token will cancel one request – Krzysztof Skowronek Nov 10 '17 at 16:52

2 Answers2

4

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.

Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • Thanks for your reply. My assumption wasn't that all requests are canceled. It was whether or not the *right* request was canceled. I know that all local variables and function params are thread-safe. – Dave Black Nov 10 '17 at 17:39
  • Well it does cancel the correct one because it stores the individual requests with their token in the state so there is no way for them to get called on the wrong one unless your code is cancelling the wrong token. – Scott Chamberlain Nov 10 '17 at 17:48
0

Just got confirmation from the Product Team at Microsoft:

Yes, it is completely safe to cancel an individual request using the cancellation token passed into the various HttpClient.SendAsync, .GetAsync, etc. methods. It does not matter that the HttpClient is "static". The cancellation token passed into the method is used for that particular request only.

Dave Black
  • 7,305
  • 2
  • 52
  • 41