29

I need to change a HttpClient.Timeout property after it made a request(s). When I try, I get an exception:

This instance has already started one or more requests. Properties can only be modified before sending the first request.

Is there any way to avoid this?

3
  • Set it before the request?? Commented May 21, 2014 at 18:18
  • @L.B., The message is clear. But I still wonder is there any trick to avoid this. geedubb, I need to change after. Commented May 21, 2014 at 18:25
  • @ValeO the value of Timeout is used to set CancelAfter on the CancellationTokenSource before the async task is started (internally). So, even if you could change it afterwards, through some "trick", it would have no effect. Commented May 21, 2014 at 18:35

5 Answers 5

21

Internally the Timeout property is used to set up a CancellationTokenSource which will abort the async operation when that timeout is reached. Since some overloads of the HttpClient methods accept CancellationTokens, we can create helper methods to have a custom timeouts for specific operations:

public async Task<string> GetStringAsync(string requestUri, TimeSpan timeout)
{
    using (var cts = new CancellationTokenSource(timeout))
    {
        HttpResponseMessage response = await _httpClient.GetAsync(requestUri, cts.Token)
        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsStringAsync();
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Worth noting that any Timeout set on the HttpClient will still be enforced. This can only reduce the time it takes for a request to timeout; it can't increase it.
13

There isn't much you can do to change this. This is just default behavior in the HttpClient implementation.

The Timeout property must be set before the GetRequestStream or GetResponse method is called. From HttpClient.Timeout Remark Section

In order to change the timeout, it would be best to create a new instance of an HttpClient.

client = new HttpClient();
client.Timeout = 20; //set new timeout

2 Comments

Beware: creating/disposing the HttpClient might cause socket exhaustion problems: aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong
I'd recommend against this. See argument in link provided by @Monsignor. You can use this approach instead: thomaslevesque.com/2018/02/25/…
6

Lack of support for custom request-level timeouts has always been a shortcoming of HttpClient in my mind. If you don't mind a small library dependency, Flurl.Http [disclaimer: I'm the author] supports this directly:

"http://api.com/endpoint".WithTimeout(30).GetJsonAsync<T>();

This is a true request-level setting; all calls to the same host use a shared HttpClient instance under the hood, and concurrent calls with different timeouts will not conflict. There's a configurable global default (100 seconds initially, same as HttpClient).

Comments

2

Expanding on the answer by @Monsignor and the reply by @andrew-bennet, this is how I solved it:

  • Leave the timeout of HTTPClient alone (100s by default) or set it up to anything larger than you will ever need, since it cannot be changed.
  • Make all the calls using HTTPClient with a custom CancellationToken with your desired timeout.
public class RestService : IRestService
{
    protected readonly HttpClient Client;
    protected static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(5);

    public async Task<T> GetAsync<T>(string uri, TimeSpan? customTimeOut = null)
    {
[...]
        CancellationToken cancellationToken = GetCancellationToken(customTimeOut);

        try
        {
                response = await Client.GetAsync(uri, cancellationToken);
        }
[...]
        return result;
    }
[...]
    private static CancellationToken GetCancellationToken(TimeSpan? customTimeOut)
    {
        CancellationTokenSource cts = customTimeOut.HasValue
            ? new CancellationTokenSource(customTimeOut.Value)
            : new CancellationTokenSource(DefaultTimeout);
        CancellationToken cancellationToken = cts.Token;
        return cancellationToken;
    }
}

Perhaps you could reuse the CancellationTokenSource, but I didn't want to complicate the sample further.

Comments

0

This .NET Framework 4.8 implementation below allows you to

  • have a singleton HttpClient, and
  • send a different timeout per request.

The way this works is, your singleton HttpClient will have an infinite timeout, but it'll use a custom HttpClientHandler that will hijack the default SendAsync behavior of your HttpClient to honor any custom "timeout property" set on the HttpRequestMessage you're trying to send. This timeout will then be enforced by a CancellationToken.

First you create a custom HttpClientHandler. (I called mine TimeoutHttpClientHandler.)

public class TimeoutHttpClientHandler : HttpClientHandler
{
    private readonly TimeSpan _defaultTimeout;

    public TimeoutHttpClientHandler(TimeSpan timeout)
    {
        _defaultTimeout = timeout;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        TimeSpan timeout = _defaultTimeout;
        if (request.Properties != null
            && request.Properties.TryGetValue(HttpClientSingleton.TimeoutPropertyKey, out var value)
            && value is TimeSpan parsedTimeout)
        {
            timeout = parsedTimeout;
        }

        using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
        {
            try
            {
                cts.CancelAfter(timeout);
                return await base.SendAsync(request, cts?.Token ?? cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
                when (!cancellationToken.IsCancellationRequested)
            {
                throw new TimeoutException();
            }
        }
    }
}

Then you instantiate your singleton HttpClient with an infinite timespan timeout, using this handler:

public static class HttpClientSingleton
{
    public const string TimeoutPropertyKey = "Timeout";

    private static readonly Lazy<HttpClient> _lazyHttpClient = new Lazy<HttpClient>(() => CreateHttpClient());

    public static HttpClient Instance
        => _lazyHttpClient.Value;

    private static HttpClient CreateHttpClient()
    {
        //I arbitrarily chose 60 seconds here; you can do whatever
        var handler = new TimeoutHttpClientHandler(TimeSpan.FromSeconds(60));

        var client = new HttpClient(handler)
        {
            Timeout = Timeout.InfiniteTimeSpan
        };
        return client;
    }
}

Then, per request, it assumes you will set the timeout like this on your HttpRequestMessage:

var httpRequest = new HttpRequestMessage(HttpMethod.Post, endpoint);

//Here is where I am setting the custom 90-second timeout, below
httpRequest.Properties[HttpClientSingleton.TimeoutPropertyKey] = TimeSpan.FromSeconds(90);
var response = await HttpClientSingleton.Instance.SendAsync(httpRequest).ConfigureAwait(false);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.