API が遅すぎると、発生したタイムアウトに応じて、.NET アプリは 2 つの例外のうち 1 つを受け取ります。
HttpClient.Timeout は TaskCanceledExceptionをスローします。
Microsoft.Extensions.Http.Resilience の標準のレジリエンス ハンドラは、Polly の TimeoutRejectedException をスローします。 それらは異なる場所に由来するため、デフォルト設定も異なり、それぞれ個別の catch ブロックが必要です。
どのタイムアウトが発生したか
| Timeout | Default | コードが取得するもの | 設定 |
|---|---|---|---|
HttpClient.Timeout |
100 秒 |
TaskCanceledException、TimeoutException を InnerException として持つ (.NET 5 以降) |
HttpClient.Timeout |
| 標準ハンドラー試行タイムアウト | 1 回の試行につき 10 秒 | 最初は何も起こらない: ハンドラーが再試行する | AddStandardResilienceHandler(options => ...) |
| 標準ハンドラーの合計タイムアウト | 30 秒 (再試行の回数をすべて含みます) | Polly.Timeout.TimeoutRejectedException |
AddStandardResilienceHandler(options => ...) |
HttpClient.Timeout
HttpClient.Timeout は、 HttpClient インスタンスが送信するすべての要求に適用されます。 1 つの要求に対して別のタイムアウトを設定するには、独自のタイムアウトを持つ CancellationTokenSource から CancellationToken を渡してください。 2 つのうち、より短い方が適用されます。
Timeout.InfiniteTimeSpan と設定してオフにします。
.NET 5 以降では、タイムアウトが発生すると、TimeoutException を含む TaskCanceledException がスローされます。 以前のバージョンの .NET Core では、内部例外は存在しません。 .NET Framework では、代わりに HttpRequestException が表示されます。 詳細については、HttpClient.Timeout および HttpClient クラスを使用した HTTP 要求の作成 に関するページを参照してください。
TaskCanceledException は、誰かが要求をキャンセルしたことを意味します。これにはたとえば、ページを閉じたユーザーなどが該当します。 タイムアウトとキャンセルを見分けるには、ex.InnerException is TimeoutException を確認するか、自身のトークンがキャンセルされていないかを確認してください。
標準のレジリエンス ハンドラー
AddStandardResilienceHandler() は、レートリミッター、合計タイムアウト、再試行、サーキット ブレーカー、および試行タイムアウトをチェーンします。 1 回の試行に 10 秒以上かかった場合、試行タイムアウトによってその試行はキャンセルされ、再試行の戦略によって再試行が行われます。再試行は最大 3 回まで行われ、2 秒から開始して、指数バックオフとジッターが適用されます。 再試行を含めた要求全体の処理に 30 秒以上かかった場合、タイムアウトで要求はキャンセルされ、コードでは TimeoutRejectedException が発生します。
TimeoutRejectedException は Exceptionから派生します。 これは TimeoutException でも HttpRequestExceptionでもなく、 catch (HttpRequestException) ブロックではキャッチされません。 既定値の完全な一覧については、「 標準の回復性ハンドラーの既定値」を参照してください。
たとえば、標準ハンドラーのデフォルト設定を使用している .NET 10 アプリが、1 回の応答につき 11~15 秒かかる API を呼び出した場合、30 秒後にアプリは TimeoutRejectedException を取得し、その catch (HttpRequestException) ブロックは実行されません。
HttpClient タイムアウトを処理する方法
- API を呼び出す場合は、両方の例外をキャッチします。 標準ハンドラーを使用する場合は、
TimeoutRejectedExceptionをキャッチしてください。HttpClient.Timeoutが発生した際に、TaskCanceledExceptionをキャッチします。 - タイムアウトとキャンセルを区別します。 内部例外が
TaskCanceledExceptionの場合にのみ、TimeoutExceptionをタイムアウトとして扱います。 呼び出し元がキャンセルしたら、静かに停止します。 - API に合ったタイムアウトを選択します。 API の処理に 10 秒以上かかることが頻繁にある場合は、
AddStandardResilienceHandler(options => ...)で「再試行タイムアウト」と「合計タイムアウト」を変更します。 - API によって安全と判断できる場合を除き、
POSTまたはPATCHを再試行しないでください。 標準ハンドラーは、既定ですべてのメソッド (POSTを含む) を再試行します。options.Retry.DisableForUnsafeHttpMethods()を呼び出すと、POST、PATCH、PUT、DELETE、CONNECTを除外します。options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch)では、べき等なPUTとDELETEの再試行を継続します。 - 何が起こったかをユーザーに伝えてください。 一般的なエラーではなく、「サービスが遅いので、再試行してください」と表示します。
using Polly.Timeout;
public async Task<string?> GetForecastAsync(HttpClient client, CancellationToken cancellationToken)
{
try
{
return await client.GetStringAsync("https://api.contoso.com/forecast", cancellationToken);
}
catch (TimeoutRejectedException)
{
// Standard resilience handler: total timeout expired after all retries
return null;
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
// HttpClient.Timeout expired
return null;
}
catch (HttpRequestException)
{
// Network error, or an error status code after all retries
return null;
}
}
アプリがタイムアウトを処理することをテストする方法
開発中にタイムアウトが発生することはほとんどないため、 catch ブロックが実行されることはほとんどありません。 テスト方法は、ユーザーより先にバグを見つけられるかどうかを決定します。
| Approach | 見つけたもの | 欠落しているもの |
|---|---|---|
| 運用環境の待機 | 実際のタイムアウト | すべて、ユーザーがクリックするまで |
| テストで API をモックするか、コーディング エージェントにモックを記述させる | モックが正しい例外をスローした場合に、 catch ブロックを実行するかどうか |
実際の HttpClient セットアップ、回復性ハンドラーの再試行、および実際にスローされる例外。 アプリでは、モックに到達するためにテスト専用のスイッチも必要です。 |
| 実際の API を呼び出し、処理が遅いことを期待します | 実際の動作 | API を意図的に低速にすることはできません |
| アプリの実際のトラフィックをインターセプトし、応答を遅らせる | 実際の HttpClient、レジリエンス ハンドラー、例外 |
アプリ自体には何の変化もないため、コードを単独でテストすることにはなりません。 それ用に単体テストを取っておいてください。 |
アプリで試す
Dev Proxyは、API に対するアプリの要求をインターセプトし、LatencyPlugin で応答を遅延させます。 .NETはシステム プロキシを使用するため、コードを変更する必要はありません。 次の使用例は、標準ハンドラーの 10 秒の試行タイムアウトよりも長い時間、各応答を 11 ~ 15 秒遅延します。
https://api.contoso.comを、アプリが呼び出す API の URL に置き換えます。
ファイル: devproxyrc.json
{
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/rc.schema.json",
"plugins": [
{
"name": "LatencyPlugin",
"enabled": true,
"pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll",
"configSection": "slowApi"
}
],
"urlsToWatch": [
"https://api.contoso.com/*"
],
"slowApi": {
"$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v3.3.1/latencyplugin.schema.json",
"minMs": 11000,
"maxMs": 15000
}
}
devproxy --config-file devproxyrc.json を使用して Dev Proxy を起動し、アプリを実行します。 各試行がタイムアウトすると、ハンドラーが再試行を行い、30 秒後にコードに TimeoutRejectedException が返されます。 代わりに HttpClient.Timeout をテストするには、minMs 設定し、構成したタイムアウトよりも長い値に設定します。 設定の詳細については、.NET アプリケーションでの Dev Proxy の使用を参照してください。 Dev Proxy をインストールするには、「Dev Proxy のセットアップ」を参照してください。
次のステップ
参照
Dev Proxy