10

I'm trying to set a default timeout for my HttpClient calls to 5 seconds.

I've done this via CancellationTokenSource.

Here's the pertinent bit of code:

var cancellationToken = new CancellationTokenSource();
cancellationToken.CancelAfter(TimeSpan.FromSeconds(5));
var result = _httpClient.SendAsync(request, cancellationToken.Token);

Works as i expected in terms of the calling code getting a "Task was cancelled" error (i tested in a .NET 4.7 console app), but i noticed in Fiddler the request was still running for 1 minute, until it finally gave up:

enter image description here

Can someone explain this behaviour?

I would expect the underlying request to also get cancelled when the cancellation is triggered.

_httpClient is instantiated like: new HttpClient { BaseAddress = baseAddress }

I know there's the the Timeout setting, but not sure if I should be using that or cancellation tokens? My guess is Timeout is for the non-async/await cases?

huysentruitw
  • 27,376
  • 9
  • 90
  • 133
RPM1984
  • 72,246
  • 58
  • 225
  • 350
  • 3
    There's no "cancel this request" action in HTTP 1.1, outside of tearing down the whole connection - which I'm guessing HttpClient doesn't do in order to support keepalive/connection reuse. – Damien_The_Unbeliever Aug 07 '17 at 09:03
  • @Damien_The_Unbeliever ahh...of course..totally forgot about `Keep-Alive`. BTW - you _can_ set `Keep-Alive` to false in HTTP client (default is true, for good reason im sure) – RPM1984 Aug 07 '17 at 23:43
  • 2
    @Damien_The_Unbeliever but having said that - if we can't cancel the underlying _request_ (not connection, but _request_), then what is the point of cancellation tokens? Just making the client (browser, or in my example console app) not care about the result anymore? (but the request may still run in the background) – RPM1984 Aug 08 '17 at 00:12
  • As I said, there's no cancel concept in HTTP. Even *if* you tear down your connection, there's no guarantee on whether the server will abort its processing or run to completion. I'd say in this case all you're gaining is the ability to stop waiting for a response, not an ability to abort processing (which, as I say, isn't defined in HTTP) – Damien_The_Unbeliever Aug 08 '17 at 06:32
  • what happens if you manually dispose the httpClient object? – David Haim Aug 08 '17 at 08:05
  • If you are also in control of the server, then you can pass the timeout to the server as a parameter, and have it do it's own timing out. – Ben Oct 15 '18 at 10:17

2 Answers2

3

As Damien said in the comments, HttpClient re-uses connections as much as possible, hence the reason why the connection is not closed on cancel.

When canceling a request like that, the HttpClient will just stop sending/receiving data to/from the other end. It will not send anything to inform the other end that it was cancelled. So the timeout you see of 1 minute depends on the behavior of the other end of your connection.

Also, if you want to cancel each request after 5 seconds, you can as well set the Timeout property of _httpClient to TimeSpan.FromSeconds(5). The behavior will be exactly the same (a TaskCanceledException will be thrown if the other end doesn't respond within 5 seconds).

huysentruitw
  • 27,376
  • 9
  • 90
  • 133
1

If anyone is interested, you can try the following approach to applying your own timeout per HttpClient request. It seems to work for me, restricting the SendAsync() to 2 seconds and returning immediately when the timeout occurs:

private async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, TimeSpan? timeout = null)
{
    if (timeout is null)
    {
        return await _httpClient.SendAsync(request);
    }
    else
    {
        using (var cts = new CancellationTokenSource(timeout.Value))
        {
            var sendTask = _httpClient.SendAsync(request);

            while (!sendTask.IsCompleted)
            {
                cts.Token.ThrowIfCancellationRequested();
                await Task.Delay(10).ConfigureAwait(false);
            }

            return await sendTask.ConfigureAwait(false);
        }
    }
}
Brandon Minnick
  • 13,342
  • 15
  • 65
  • 123
Martin Lottering
  • 1,624
  • 19
  • 31