當 API 太慢時,你的 .NET 應用程式會取決於觸發的是哪一種逾時,而遇到兩種例外中的其中一種。
HttpClient.Timeout 擲回 TaskCanceledException。 來自 Microsoft.Extensions.Http.Resilience 的標準韌性處理常式會從 Polly 擲回 TimeoutRejectedException。 它們來自不同地方,有不同的預設值,需要分開 catch 的區塊。
哪個逾時已觸發
| 暫停 | 預設 | 你的程式碼得到什麼 | 您設定它的位置 |
|---|---|---|---|
HttpClient.Timeout |
100 秒 |
TaskCanceledException,其中 TimeoutException 為其 InnerException(.NET 5 及更新版本) |
HttpClient.Timeout |
| 標準處理常式嘗試的逾時時間 | 每次嘗試10秒 | 一開始沒有任何反應:處理常式重新嘗試 | AddStandardResilienceHandler(options => ...) |
| 標準處理常式總逾時時間 | 30秒,包含所有重試 | Polly.Timeout.TimeoutRejectedException |
AddStandardResilienceHandler(options => ...) |
HttpClient.Timeout
HttpClient.Timeout 適用於 HttpClient 實例所發送的每一個請求。 若要對單一請求使用不同的逾時,請傳遞來自具有自身逾時設定之 CancellationToken 的 CancellationTokenSource。 兩者中較短的那個適用。 將 Timeout.InfiniteTimeSpan 設為關閉。
在 .NET 5 及後續版本中,逾時會擲出內含 TimeoutException 的 TaskCanceledException。 在較早版本的 .NET Core 中,沒有這個內部例外。 在 .NET Framework 中,你會改為得到HttpRequestException。 詳情請參閱 HttpClient.Timeout 及 使用 HttpClient 類別 Make HTTP 請求。
TaskCanceledException 也表示有人取消了請求,例如關閉頁面的使用者。 要區分逾時與取消,請檢查 ex.InnerException is TimeoutException,或檢查你自己的權杖是否已取消。
標準韌性處理器
AddStandardResilienceHandler() 串接速率限制器、總逾時、重試、斷路器及嘗試逾時。 當一次嘗試超過10秒時,嘗試逾時會取消該次嘗試,而重試策略會再次嘗試:最多重試3次,並伴隨指數退避與抖動,從2秒開始。 當整個請求(包括重試)超過 30 秒時,總逾時會取消該請求,你的程式碼會收到 TimeoutRejectedException。
TimeoutRejectedException 衍生自 Exception。 它既不是一個 TimeoutException,也不是一個 HttpRequestException,所以 catch (HttpRequestException) 區塊不會接住它。 完整預設值清單請參閱 標準韌性處理程序預設值。
舉例來說,當一個使用標準處理器預設值的 .NET 10 應用程式呼叫一個每次回應需時 11 到 15 秒的 API,應用程式會在 30 秒後收到錯誤TimeoutRejectedException,且其catch (HttpRequestException)區塊不會執行。
如何處理 HttpClient 逾時
- 在呼叫 API 的地方同時捕捉這兩個例外。 如果你用標準處理常式,就抓
TimeoutRejectedException。 為HttpClient.Timeout捕捉TaskCanceledException。 - 分辨逾時和取消。 只有當 a
TaskCanceledException的內部例外是 aTimeoutException時,才會將其視為逾時。 當來電者取消時,靜默停止。 - 選擇符合 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 配置、韌性處理常式的重試,以及它實際拋出的例外。 你的應用程式也需要一個只供測試用的開關來連到 mock 服務。 |
| 呼叫真正的 API,希望它很慢 | 實際行為 | 你無法按需讓 API 變慢 |
| 攔截應用程式的真實流量並延遲回應 | 你的實際 HttpClient、韌性處理常式,以及例外狀況 |
你的應用程式完全不需要變更,因此無法在隔離環境中測試你的程式碼。 把那個留給單元測試。 |
在你的應用程式中試試看
Dev Proxy 會攔截你應用程式對 API 的請求,並用 LatencyPlugin 延遲回應。 .NET 會用系統代理,所以你不需要改程式碼。 此範例會延遲每個回應 11 到 15 秒,比標準處理器的 10 秒嘗試逾時還長。 將 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 設定為高於你所設定的逾時值。 關於設定細節,請參見「使用 Dev Proxy 搭配 .NET 應用程式」。 要安裝 Dev Proxy,請參見 設定 Dev Proxy。