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