Introduction

When an HttpClient request is cancelled (either explicitly via CancellationToken or implicitly via Timeout), it throws OperationCanceledException or TaskCanceledException. This is not a network error: it is an expected cancellation that should be handled differently from other failures. Mishandling cancellation leads to incorrect error logging, unnecessary retries, and resource leaks.

Symptoms

  • System.Threading.Tasks.TaskCanceledException: The operation was canceled
  • System.OperationCanceledException: The operation was canceled
  • Cancellation logged as an error instead of expected behavior
  • Timeout fires and request is cancelled mid-stream
  • Retry logic retries cancelled requests unnecessarily

Example error: `` System.Threading.Tasks.TaskCanceledException: The operation was canceled. ---> System.IO.IOException: Unable to read data from the transport connection: Operation canceled. ---> System.Net.Sockets.SocketException (125): Operation canceled at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage, CancellationToken)

Common Causes

  • HttpClient.Timeout (default 100 seconds) expires
  • CancellationTokenSource.CancelAfter() fires
  • ASP.NET Core request cancelled by client (browser navigation)
  • Retry policy does not distinguish cancellation from failure
  • Hosted service cancelled during shutdown

Step-by-Step Fix

  1. 1.Distinguish cancellation from other errors:
  2. 2.```csharp
  3. 3.public async Task<ApiResponse> GetDataAsync(CancellationToken ct = default)
  4. 4.{
  5. 5.try
  6. 6.{
  7. 7.using var response = await _httpClient.GetAsync("/api/data", ct);
  8. 8.response.EnsureSuccessStatusCode();
  9. 9.return await response.Content.ReadFromJsonAsync<ApiResponse>(ct);
  10. 10.}
  11. 11.catch (OperationCanceledException)
  12. 12.{
  13. 13.// This is expected - do not log as error, do not retry
  14. 14._logger.LogDebug("Request was cancelled");
  15. 15.throw; // Re-throw for the caller to handle
  16. 16.}
  17. 17.catch (HttpRequestException ex)
  18. 18.{
  19. 19.// This is a real network error - log and potentially retry
  20. 20._logger.LogError(ex, "HTTP request failed");
  21. 21.throw;
  22. 22.}
  23. 23.}
  24. 24.`
  25. 25.Configure appropriate timeouts:
  26. 26.```csharp
  27. 27.builder.Services.AddHttpClient("api", client =>
  28. 28.{
  29. 29.client.BaseAddress = new Uri("https://api.example.com/");
  30. 30.client.Timeout = TimeSpan.FromSeconds(30); // Not the default 100s
  31. 31.client.DefaultRequestHeaders.Add("Accept", "application/json");
  32. 32.})
  33. 33..ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
  34. 34.{
  35. 35.PooledConnectionLifetime = TimeSpan.FromMinutes(5),
  36. 36.MaxConnectionsPerServer = 10
  37. 37.});
  38. 38.`
  39. 39.Handle cancellation in retry policies (Polly):
  40. 40.```csharp
  41. 41.var retryPolicy = HttpPolicyExtensions
  42. 42..HandleTransientHttpError() // 5xx, 408, network errors
  43. 43..OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests)
  44. 44..WaitAndRetryAsync(3, retryAttempt =>
  45. 45.TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))
  46. 46.// Do NOT retry on cancellation
  47. 47..Handle<OperationCanceledException>(ex => !(ex is TaskCanceledException))
  48. 48..WaitAndRetryAsync(3, retryAttempt =>
  49. 49.TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

// Usage var result = await retryPolicy.ExecuteAsync( async (ct) => await httpClient.GetAsync("/data", ct), cancellationToken); ```

  1. 1.Link cancellation tokens for combined timeouts:
  2. 2.```csharp
  3. 3.public async Task<ApiResponse> GetDataWithTimeoutAsync(CancellationToken ct = default)
  4. 4.{
  5. 5.// Combine the caller's token with a 30-second timeout
  6. 6.using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
  7. 7.cts.CancelAfter(TimeSpan.FromSeconds(30));

try { return await _httpClient.GetFromJsonAsync<ApiResponse>( "/api/data", cts.Token); } catch (OperationCanceledException) { if (ct.IsCancellationRequested) { _logger.LogDebug("Request cancelled by caller"); } else { _logger.LogWarning("Request timed out after 30 seconds"); } throw; } } ```

Prevention

  • Always catch OperationCanceledException separately from other exceptions
  • Set HttpClient.Timeout to a value appropriate for your use case
  • Use CancellationTokenSource.CreateLinkedTokenSource for combined timeouts
  • Do not retry on cancellation: it is intentional, not a transient failure
  • Use IHttpClientFactory for proper HttpClient lifecycle management
  • Pass CancellationToken through the entire call chain
  • Handle ASP.NET Core HttpContext.RequestAborted for request-scoped cancellation