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 つの要求に対して異なるタイムアウトを使用するには、独自のタイムアウトを持つ CancellationToken から CancellationTokenSource を渡します。 2つのうち短い方が適用されます。
Timeout.InfiniteTimeSpanをオフに設定してください。
.NET 5 以降では、タイムアウトにより、TaskCanceledException を内部に含む TimeoutException がスローされます。 以前のバージョンの .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 アプリが応答あたり 11 秒から 15 秒かかる API を呼び出すと、アプリでは 30 秒後にTimeoutRejectedExceptionが発生し、catch (HttpRequestException) ブロックは実行されません。
HttpClient タイムアウトを処理する方法
- API を呼び出す場合は、両方の例外をキャッチします。 標準ハンドラーを使用する場合は、
TimeoutRejectedExceptionをキャッチしてください。TaskCanceledExceptionのためにHttpClient.Timeoutをキャッチします。 - タイムアウトとキャンセルを区別する 内部例外が
TaskCanceledExceptionの場合にのみ、TimeoutExceptionをタイムアウトとして扱います。 呼び出し元がキャンセルしたら、静かに停止します。 - API に合ったタイムアウトを選択します。 API に 10 秒を超える時間がかかることが多い場合は、試行と合計タイムアウトを
AddStandardResilienceHandler(options => ...)で変更してください。 - API によって安全と判断できる場合を除き、
POSTまたはPATCHを再試行しないでください。 標準ハンドラーは、既定ですべてのメソッド (POSTを含む) を再試行します。POSTを呼び出してPATCH、PUT、DELETE、CONNECT、およびoptions.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch)を除外するか、またはoptions.Retry.DisableForUnsafeHttpMethods()を呼び出して冪等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