← 中文资源

简体中文版本

C# HttpClient TaskCanceledException — 超时并非真实错误

通过使用 CancellationToken 区分请求超时与调用方取消,并保留内部的 TimeoutException,修复 HttpClient 的 TaskCanceledException。

阅读英文原文

症状

错误“C# HttpClient TaskCanceledException — 超时并非真实错误”指向一个明确的失效模式。下面依次给出症状、定位该原因的检查命令、根本原因、修复方案,以及证明修复有效的验证步骤。

可观察到的症状包括:

  • System.Threading.Tasks.TaskCanceledException: A task was canceled
  • 仅对慢端点出现该错误
  • 重试逻辑把超时与用户取消同等对待

诊断

先在隔离环境中复现故障,再按顺序执行以下检查,逐步缩小范围。

步骤 1

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

若调用方 token 未取消,异常源自 HttpClient.Timeout,其内部异常为 TimeoutException。

步骤 2

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

内部为 TimeoutException 即确认超时;内部为 null 则指向调用方取消。

常见诱因(按出现频率排序):

  • HttpClient.Timeout 到期,表现为 TaskCanceledException
  • 调用方的 CancellationToken 触发,主动取消了请求
  • 超时设置过小,正常延迟也被当作失败

根因

.NET 把 HttpClient 超时与调用方取消都报告为 TaskCanceledException。只有 CancellationToken 状态与内部 TimeoutException 能区分二者,忽视它们的代码会混淆主动取消与真实超时。

修复

使用 HttpClient.Timeout 或链接的 CancellationTokenSource,捕获 TaskCanceledException 时用 when (!ct.IsCancellationRequested) 过滤以判定超时,并原样传播真正的取消。

注意:对取消重试会浪费工作并延迟关闭;仅对判定为超时及瞬时错误重试。

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

验证

模拟慢端点并确认日志判定为超时;用 token 取消并确认报告为取消而非重试。

修复后补充一条回归测试,复现原始症状,防止问题再次出现。

这类故障通常是环境问题而非纯逻辑错误,因此在缺陷记录中应写明完整命令、环境变量与工作目录。请用最小的脚本复现,待最小用例清楚后再扩大范围。为这类故障准备一份简短的排查清单,能显著减少下次排障时间。