← Debugging Guides

阅读中文版本

C# HttpClient TaskCanceledException — Timeout Not an Error

Fix HttpClient TaskCanceledException by distinguishing a request timeout from caller cancellation using CancellationToken, keeping its inner TimeoutException.

intermediatedebuggingcsharphttpclienttimeout

Symptoms

  • System.Threading.Tasks.TaskCanceledException: A task was canceled
  • The error appears only for slow endpoints
  • Retry logic treats a timeout the same as a user cancel

Possible causes

  • The HttpClient.Timeout elapsed, which surfaces as TaskCanceledException
  • A caller CancellationToken fired, cancelling the request intentionally
  • A timeout too small for the endpoint makes normal latency look like a failure

Diagnosis steps

Distinguish timeout from caller cancellation

catch (TaskCanceledException ex) when (!ct.IsCancellationRequested)

Interpretation

If the caller token is not cancelled, the exception came from HttpClient.Timeout; the inner exception is a TimeoutException.

Inspect the inner exception

dotnet run 2>&1 | grep -A 3 "TaskCanceledException"

Interpretation

A TimeoutException inner exception confirms a timeout; a null inner points to caller cancellation.

Root cause

.NET reports HttpClient timeout and caller cancellation both as TaskCanceledException. Only the CancellationToken state and the inner TimeoutException tell them apart, so code that ignores those conflates deliberate cancellation with a real timeout.

Fix

Use HttpClient.Timeout or a linked CancellationTokenSource, catch TaskCanceledException with a `when (!ct.IsCancellationRequested)` filter to classify timeouts, and propagate genuine cancellation unchanged.

Risk note

Retrying on cancellation wastes work and delays shutdown; retry only classified timeouts and transient errors.

Verify the fix

Simulate a slow endpoint and confirm the log classifies a timeout; cancel via token and confirm it is reported as cancellation, not a retry.

Symptoms

The error “C# HttpClient TaskCanceledException — Timeout Not an Error” points at a specific failure mode. This guide documents the symptoms you can observe, the commands that isolate the cause, the root cause, the fix, and the checks that prove the fix works.

Observed symptoms include:

  • System.Threading.Tasks.TaskCanceledException: A task was canceled
  • The error appears only for slow endpoints
  • Retry logic treats a timeout the same as a user cancel

Reproduce and Diagnose

Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.

Step 1: Distinguish timeout from caller cancellation

catch (TaskCanceledException ex) when (!ct.IsCancellationRequested)

If the caller token is not cancelled, the exception came from HttpClient.Timeout; the inner exception is a TimeoutException.

Step 2: Inspect the inner exception

dotnet run 2>&1 | grep -A 3 "TaskCanceledException"

A TimeoutException inner exception confirms a timeout; a null inner points to caller cancellation.

Likely contributors, in order of frequency:

  • The HttpClient.Timeout elapsed, which surfaces as TaskCanceledException
  • A caller CancellationToken fired, cancelling the request intentionally
  • A timeout too small for the endpoint makes normal latency look like a failure

Root Cause

.NET reports HttpClient timeout and caller cancellation both as TaskCanceledException. Only the CancellationToken state and the inner TimeoutException tell them apart, so code that ignores those conflates deliberate cancellation with a real timeout.

The Fix

Use HttpClient.Timeout or a linked CancellationTokenSource, catch TaskCanceledException with a when (!ct.IsCancellationRequested) filter to classify timeouts, and propagate genuine cancellation unchanged.

Watch out: Retrying on cancellation wastes work and delays shutdown; retry only classified timeouts and transient errors.

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
{
    var res = await client.GetAsync(url, cts.Token);
    res.EnsureSuccessStatusCode();
}
catch (TaskCanceledException ex) when (!cts.IsCancellationRequested)
{
    // timeout: inner exception is a TimeoutException
    throw new TimeoutException($"request to {url} timed out", ex);
}
// genuine cancellation flows through unchanged

Verify and Prevent

Simulate a slow endpoint and confirm the log classifies a timeout; cancel via token and confirm it is reported as cancellation, not a retry.

Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.

This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.