.NET HttpClient 逾時:TaskCanceledException 與 TimeoutRejectedException

當 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 逾時

  1. 在呼叫 API 的地方同時捕捉這兩個例外。 如果你用標準處理常式,就抓 TimeoutRejectedException。 為HttpClient.Timeout捕捉TaskCanceledException。
  2. 分辨逾時和取消。 只有當 a TaskCanceledException 的內部例外是 a TimeoutException 時,才會將其視為逾時。 當來電者取消時,靜默停止。
  3. 選擇符合 API 的超時時間。 如果 API 經常需要超過 10 秒,請在 AddStandardResilienceHandler(options => ...) 中更改嘗試逾時和總逾時。
  4. 除非 API 表示可安全地重試,否則不要重試 POST 或 PATCH。 標準處理器預設會重試所有方法,包括 POST。 呼叫 options.Retry.DisableForUnsafeHttpMethods() 以排除 POST、PATCH、PUT、DELETE 和 CONNECT,或呼叫 options.Retry.DisableFor(HttpMethod.Post, HttpMethod.Patch) 以持續重試冪等 PUT 和 DELETE。
  5. 告訴使用者發生了什麼事。 顯示「服務速度緩慢,請再試一次」而不是一般錯誤。
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。

下一步

也請參閱