概要
System.Threading.Tasks.TaskCanceledExceptionは、非同期タスクがキャンセルされた場合に発生します。HttpClientのタイムアウトやCancellationTokenによるキャンセルが主な原因です。
エラーメッセージ
System.Threading.Tasks.TaskCanceledException: A task was canceled.
at System.Net.Http.HttpClient.HandleFailure(Exception e, Boolean telemetryStarted, HttpResponseMessage response, CancellationTokenSource cts, CancellationToken cancellationToken, CancellationTokenSource pendingRequestsCts)
System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 100 seconds elapsing.
原因
- HttpClientのタイムアウト: デフォルト100秒のタイムアウトを超過
- CancellationTokenによるキャンセル: 明示的なキャンセル要求
- サーバーの応答遅延: APIやデータベースの応答が遅い
- HttpClientの不正なライフサイクル: usingブロック内でリクエスト完了前にDispose
解決策
1. HttpClientのタイムアウトを調整する
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // NG: デフォルトの100秒タイムアウト
var client = new HttpClient();
// OK: タイムアウトを明示的に設定
var client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(30) // 短めに設定
};
// 特定のリクエストだけタイムアウトを変えたい場合
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
var response = await client.GetAsync("https://api.example.com/data", cts.Token);
}
catch (TaskCanceledException) when (!cts.IsCancellationRequested)
{
Console.WriteLine("HttpClientのタイムアウト");
}
catch (TaskCanceledException)
{
Console.WriteLine("明示的なキャンセル");
}
|
2. リトライロジックを実装する(Polly使用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // NuGet: Microsoft.Extensions.Http.Polly
using Polly;
using Polly.Extensions.Http;
// Program.cs(.NET 6以降)
builder.Services.AddHttpClient("MyApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(GetRetryPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.Or<TaskCanceledException>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 指数バックオフ
onRetry: (outcome, timespan, retryCount, context) =>
{
Console.WriteLine($"リトライ {retryCount}回目: {timespan.TotalSeconds}秒後");
});
}
|
3. CancellationTokenを正しく扱う
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public class DataService
{
private readonly HttpClient _client;
public async Task<string> FetchDataAsync(CancellationToken cancellationToken = default)
{
try
{
var response = await _client.GetAsync(
"https://api.example.com/data",
cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)
{
// ユーザーによる明示的なキャンセル
Console.WriteLine("リクエストがキャンセルされました");
throw; // 呼び出し元に伝播
}
catch (TaskCanceledException)
{
// タイムアウト
Console.WriteLine("リクエストがタイムアウトしました");
throw new TimeoutException("APIの応答がタイムアウトしました");
}
}
}
|
4. IHttpClientFactoryを使用する
HttpClientのライフサイクルを適切に管理する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // Program.cs
builder.Services.AddHttpClient("MyApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
// サービスクラス
public class ApiService
{
private readonly IHttpClientFactory _factory;
public ApiService(IHttpClientFactory factory)
{
_factory = factory;
}
public async Task<string> GetDataAsync(CancellationToken ct)
{
using var client = _factory.CreateClient("MyApi");
var response = await client.GetAsync("/data", ct);
return await response.Content.ReadAsStringAsync(ct);
}
}
|
5. タイムアウトとキャンセルを区別する
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public static async Task<T?> SafeRequestAsync<T>(
Func<CancellationToken, Task<T>> request,
TimeSpan timeout,
CancellationToken externalToken = default)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(externalToken);
cts.CancelAfter(timeout);
try
{
return await request(cts.Token);
}
catch (OperationCanceledException) when (externalToken.IsCancellationRequested)
{
Console.WriteLine("外部からキャンセルされました");
return default;
}
catch (OperationCanceledException)
{
Console.WriteLine($"タイムアウト ({timeout.TotalSeconds}秒)");
return default;
}
}
|
関連エラー