9

I'm running into an issue with the .NET HttpClient class (.NET 4.5.1, System.Net.Http v4.0.0.0). I'm calling HttpClient.GetAsync, passing in a CancellationToken (as part of a Nuget package that abstracts calls between webservices). If the token has been cancelled before the call is made, the request goes through without throwing an exception. This behavior doesn't seem correct.

My test (incomplete, not fully written - no exception check):

[TestMethod]
public async Task Should_Cancel_If_Cancellation_Token_Called()
{
    var endpoint = "nonexistent";
    var cancellationTokenSource = new CancellationTokenSource();

    var _mockHttpMessageHandler = new MockHttpMessageHandler();
    _mockHttpMessageHandler
        .When("*")
        .Respond(HttpStatusCode.OK);

    var _apiClient = new ApiClientService(new HttpClient(_mockHttpMessageHandler));
    cancellationTokenSource.Cancel();

    var result = await _apiClient.Get<string>(endpoint, null, cancellationTokenSource.Token);
}

The method I'm testing:

public async Task<T> Get<T>(string endpoint, IEnumerable<KeyValuePair<string, string>> parameters = null, CancellationToken cancellationToken = default(CancellationToken))
{
    var builder = new UriBuilder(Properties.Settings.Default.MyEndpointHost + endpoint);
    builder.Query = buildQueryStringFromParameters(parameters);

    _httpClient.DefaultRequestHeaders.Accept.Clear();
    _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    try
    {
        // After this, we really shouldn't continue.
        var request = await _httpClient.GetAsync(builder.Uri, cancellationToken);

        if (!request.IsSuccessStatusCode)
        {
            if (request.StatusCode >= HttpStatusCode.BadRequest && request.StatusCode < HttpStatusCode.InternalServerError)
            {
                throw new EndpointClientException("Service responded with an error message.", request.StatusCode, request.ReasonPhrase);
            }

            if (request.StatusCode >= HttpStatusCode.InternalServerError && (int)request.StatusCode < 600)
            {
                throw new EndpointServerException("An error occurred in the Service endpoint.", request.StatusCode, request.ReasonPhrase);
            }
        }

        var json = await request.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<T>(json);
    }
    catch (Exception ex)
    {
        throw;
    }
}

I know that I can check the status of the cancellation token before calling HttpClient.GetAsync and throw if cancellation has been requested. I know that I can also register a delegate to cancel the HttpClient request. However, it seems as though passing the token to the HttpClient method should take care of this for me (or, else, what's the point?) so I'm wondering if I'm missing something. I don't have access to the HttpClient source code.

Why is HttpClient.GetAsync not checking my cancellation token and aborting its process when I pass it in?

Mike Zboray
  • 39,828
  • 3
  • 90
  • 122

1 Answers1

3

HttpClient doesn't check the cancellation token itself, it passes it on to the message handler when it calls its SendAsync method. It then registers to the continuation on the task returned from SendAsync and will set its own task as cancelled if the task returned from the message handler was cancelled.

So the problem in your scenario is in your implementation of MockHttpMessageHandler which seems doesn't check the cancellation token.

Note, that if HttpClient is called via its empty constructor, it internally uses HttpClientHandler which registers a delegate on the cancellation token that aborts the request and cancels the task.

tzachs
  • 4,331
  • 1
  • 28
  • 28
  • 1
    D'oh! It's always something simple, isn't it. This actually does make sense, though - I knew that `HttpClient` was a wrapper for the underlying handler, but somehow I didn't think about the implications when it came to the cancellation token. In this case, I guess I wouldn't really be able to test the underlying implementation unless I actually made a full HTTP request (or at least called into the actual handler). –  Dec 03 '15 at 20:49
  • You don't have to make a full HTTP request. In your mock, in the override of `SendAsync`, you can check the token in the beginning of the method. – tzachs Dec 03 '15 at 21:02
  • I'm using https://github.com/richardszalay/mockhttp for the mocking. I'll add an issue to his repo, but realized that it might be easier to actually make the request without the mock as the cancellation token should cause it to be cancelled anyway. –  Dec 03 '15 at 22:10