When you’re using the same HttpClient instance to send multiple requests, and you want to change the timeout per request, you can pass in a CancellationToken, like this:
using (var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
var response = await httpClient.GetAsync(uri, tokenSource.Token);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return content;
}
Code language: C# (cs)
You can’t change HttpClient.Timeout after the instance has been used. You have to pass in a CancellationToken instead.
There are other key points to know when trying to control HttpClient’s timeout. In this article, I’ll go into details about these key points.
Table of Contents
You can’t change HttpClient.Timeout after it’s been used
If you try to change HttpClient.Timeout after the instance has been used at least once, you’ll get the following exception:
InvalidOperationException: This instance has already started one or more requests and can only be modified before sending the first request
Since it’s best practice to reuse HttpClient instances, naturally you may think you can change the Timeout property. Everyone runs into this problem.
It may seem counterintuitive that you can’t change this property, but it makes perfect sense if you think about it. The HttpClient class was designed to be used to send multiple requests concurrently. If you could change HttpClient.Timeout, it would be thread unsafe.
For example, if you had two threads using the HttpClient instance, and both threads changed the Timeout value at the same time, then both threads would use last value the Timeout was set to. In other words, it’s a race condition. To guard against this race condition, Microsoft simply made it impossible to change the Timeout value after it’s been used.
HttpClient uses the lesser of HttpClient.Timeout and CancellationToken’s timeout
The CancellationToken doesn’t override HttpClient.Timeout. Instead, it uses the timeout with the lesser value.
In other words:
- if HttpClient.Timeout < CancellationToken’s timeout, it’ll use HttpClient.Timeout.
- if CancellationToken’s timeout < HttpClient.Timeout, it’ll use the CancellationToken’s timeout.
Keep this in mind when you’re trying to control the timeout. Since you can’t change HttpClient.Timeout after the instance has been used, this means you can’t change the timeout to a value greater than HttpClient.Timeout. So if you’re using CancellationTokens to control the timeout per request, make sure to initialize HttpClient.Timeout to a value greater than the max timeout you want to use. Note: HttpClient.Timeout defaults to 100 seconds.
The following experiment shows this behavior.
First, CancellationToken will have a 1 second timeout, and HttpClient.Timeout will be 5 seconds.
static async Task Main(string[] args)
{
string uri = "https://localhost:12345/stocks/VTSAX";
var requestTimeout = TimeSpan.FromSeconds(1);
var httpTimeout = TimeSpan.FromSeconds(5);
HttpClient httpClient = new HttpClient();
httpClient.Timeout = httpTimeout;
var stopwatch = Stopwatch.StartNew();
try
{
using (var tokenSource = new CancellationTokenSource(requestTimeout))
{
var response = await httpClient.GetAsync(uri, tokenSource.Token);
}
}
catch (TaskCanceledException)
{
Console.WriteLine($"Timed out after {stopwatch.Elapsed}");
}
}
Code language: C# (cs)
Tip: To make your code robust to random problems, such as timeouts, I suggest implementing a retry policy.
This outputs the following, indicating that it used the 1 second timeout set by the CancellationToken.
Timed out after 00:00:01.0369436
Code language: plaintext (plaintext)
Now change it so CancellationToken’s timeout > HttpClient.Timeout:
var requestTimeout = TimeSpan.FromSeconds(10);
var httpTimeout = TimeSpan.FromSeconds(5);
Code language: C# (cs)
Repeat the test. It outputs:
Timed out after 00:00:05.0449518
Code language: plaintext (plaintext)
This indicates it used the HttpClient.Timeout value.
Don’t pass an invalid timeout to the CancellationToken
If you pass in a timeout value of 0, then it’ll timeout immediately:
new CancellationTokenSource(TimeSpan.FromSeconds(0))
Code language: C# (cs)
If you try to pass in a timeout value < 0,
new CancellationTokenSource(TimeSpan.FromSeconds(-1)
Code language: C# (cs)
Then you’ll get this exception:
System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values. (Parameter ‘delay’)
at System.Threading.CancellationTokenSource..ctor(TimeSpan delay)
Make sure to guard against passing in invalid timeout values to avoid unexpected behavior. For example, you could check the timeout value and only use the CancellationToken in the request if the timeout is valid:
if (requestTimeout.TotalSeconds > 0)
{
using (var tokenSource = new CancellationTokenSource(requestTimeout))
{
var response = await httpClient.GetAsync(uri, tokenSource.Token);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return content;
}
}
Code language: C# (cs)
What if you’re already passing in a cancellation token from the outside?
Let’s say you want the user to be able to cancel the HttpClient request, so you’re already passing in a CancellationToken. And you want to be able to change the timeout per request.
You can combine these cancellation tokens by using CancellationTokenSource.CreateLinkedTokenSource(), like this:
public async Task<string> GetStock(string symbol, TimeSpan requestTimeout, CancellationToken userCancelToken)
{
try
{
using (var requestCTS = new CancellationTokenSource(requestTimeout))
{
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(requestCTS.Token, userCancelToken))
{
var response = await httpClient.GetAsync(uri, linkedCts.Token);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return content;
}
}
}
catch (TaskCanceledException)
{
if (userCancelToken.IsCancellationRequested)
{
Console.WriteLine("User canceled");
}
else
{
Console.WriteLine($"Timed out");
}
throw;
}
}
Code language: C# (cs)
You’ll get a TaskCanceledException if the user canceled or if the HttpClient request timed out. You can tell these two scenarios apart by checking if the user CancellationToken was canceled.
Note: It’s possible for both scenarios – user canceled and timeout – to be true at the same time.
HttpClient seemingly ignores configured timeouts due to slow automatic proxy detection
Let’s say you’ve set HttpClient.Timeout to 5 seconds, but it actually takes 20-30 seconds to timeout. You may be running into the “automatic proxy detection is slow” issue. If you run Fiddler (it acts as a proxy), and the problem goes way, then you’re for sure running into that problem.
Depending on your situation, one option is to turn off automatic proxy detection (for your program only).
If you’re using .NET Framework, you could do this in the app.config or web.config:
<system.net>
<defaultProxy>
<proxy bypassonlocal="true" usesystemdefault="false" />
</defaultProxy>
</system.net>
Code language: HTML, XML (xml)
If you’re using .NET Core, you may have to disable the proxy programmatically:
var config = new HttpClientHandler
{
UseProxy = false
};
httpClient = new HttpClient(config);
Code language: C# (cs)
Here are some helpful references for this proxy issue:
- Slow HttpClient timeout due to proxy (West Wind)
- Running fiddler fixes my slow proxy issue (Telerik)
Comments are closed.