The Error
Random failures calling external APIs:
System.Threading.Tasks.TaskCanceledException: A task was canceled.
---> System.TimeoutException: The operation was canceled.
Quick Fix - 1 Minute
HttpClient default timeout is 100 seconds. It throws TaskCanceledException on timeout, not TimeoutException. Increase it or fix DI.
Wrong - Creates new HttpClient each time:
public async Task CallApi()
{
using var client = new HttpClient(); // BAD - socket exhaustion
await client.GetAsync("https://api.com");
}
Right - Use IHttpClientFactory:
// Program.cs
builder.Services.AddHttpClient("ApiClient", client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
});
// Service
public class ApiService
{
private readonly IHttpClientFactory _factory;
public ApiService(IHttpClientFactory factory) => _factory = factory;
public async Task CallApi()
{
var client = _factory.CreateClient("ApiClient");
await client.GetAsync("https://api.com");
}
}
Why This Happens
1. Real timeout: External API is slow > 100s
2. Socket exhaustion: new HttpClient() in a loop opens thousands of ports. OS kills it
3. CancellationToken: Request cancelled by user closing browser
Real-World Scenario: Distinguishing Timeout vs User Cancel
TaskCanceledException has 3 causes. Check ex.CancellationToken.IsCancellationRequested to know which:
public async Task<string> CallApiSafely(CancellationToken userToken)
{
var client = _factory.CreateClient("ApiClient");
var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token;
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(userToken, timeoutToken);
try
{
var response = await client.GetAsync("https://api.com", linkedCts.Token);
return await response.Content.ReadAsStringAsync();
}
catch (OperationCanceledException ex) when (userToken.IsCancellationRequested)
{
// User closed browser or hit cancel button
_logger.LogInformation("Request cancelled by user");
throw;
}
catch (OperationCanceledException ex) when (timeoutToken.IsCancellationRequested)
{
// Your 10s timeout hit - API is slow
_logger.LogWarning("API timeout after 10s");
throw new TimeoutException("External API timed out", ex);
}
}
Why this works: HttpClient throws the same exception for timeout and user cancel. Linked tokens let you tell them apart so you can retry timeouts but not user cancels.
Related Fixes You Should Know
HttpClient timeouts usually mean you have these issues too:
- HttpClient Socket Exhaustion - Using
new HttpClient()instead of factory. Causes thousands of TIME_WAIT sockets. OS kills new connections = TaskCanceledException. - Polly Retry Not Working - You added Polly but timeouts still fail. You need
.AddPolicyHandlerAFTER settingclient.Timeout. - BackgroundService CancellationToken - Forgetting to pass
stoppingTokento HttpClient in BackgroundService. App hangs 100s on shutdown. - HttpClient DelegatingHandler DI Scope - Injecting scoped services into Singleton HttpClient handler causes
ObjectDisposedExceptionmasked as timeout.
FAQ
Q: Why does HttpClient throw TaskCanceledException instead of TimeoutException?
By design. HttpClient.Timeout uses an internal CancellationTokenSource. When it fires, it cancels the request. Same exception as user cancellation. Check the token to differentiate.
Q: What is the default HttpClient timeout in.NET 8?
100 seconds. Way too long for web APIs. Set it to 5-30 seconds based on your SLA. Use Polly to retry 3 times instead of waiting 100s once.
Best Practice for.NET 8
1. Always use AddHttpClient: Manages socket lifetime
2. Set explicit timeout: client.Timeout = TimeSpan.FromSeconds(10);
3. Use Polly: Add retry policy for transient failures
builder.Services.AddHttpClient("ApiClient")
.AddTransientHttpErrorPolicy(p =>
p.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2)));
No comments yet. Be the first to share your thoughts!