IHttpClientFactory
使用常見問題
在本文中,您將瞭解在使用 IHttpClientFactory
來建立 HttpClient
執行個體時,可能會遇到的幾種最常見問題。
IHttpClientFactory
是一種很便利的方法,可用於設定多個 HttpClient
DI 容器的組態、設定記錄、設定復原策略等。 IHttpClientFactory
也會封裝 和 HttpMessageHandler
實例的HttpClient
存留期管理,以避免發生套接字耗盡和 DNS 變更遺失等問題。 如需概觀了解如何在 .NET 應用程式使用 IHttpClientFactory
,請參閱搭配 .NET 使用 IHttpClientFactory。
由於 IHttpClientFactory
與 DI 整合的本質複雜,您可能會遇到幾種難以理解和排除的問題。 本文所列的案例也包含建議,可供您主動應用,避免發生潛在問題。
HttpClient
與 Scoped
存留期無關
如果您需要從 HttpMessageHandler
存取任何已設範圍的服務,例如 HttpContext
,或已設範圍的快取,可能會遇到問題。 儲存在該處的資料可能會「消失」,或者反過來說,在不應「保存」時繼續保存。 這種現象的原因是應用程式內容與處理常式執行個體之間的相依性插入 (DI) 範圍不符,且是 IHttpClientFactory
的已知限制。
IHttpClientFactory
會為每個 HttpMessageHandler
執行個體建立個別 DI 範圍。 這些處理常式範圍與應用程式內容範圍 (例如 ASP.NET Core 傳入要求範圍,或使用者建立的手動 DI 範圍) 並不同,因此它們不會共用已設範圍的服務執行個體。
此限制的結果是:
- 在已設範圍的服務「外部」快取的任何資料都無法在
HttpMessageHandler
內使用。 - 在
HttpMessageHandler
或其已設範圍的相依性「內部」快取的任何資料,都可以從多個應用程式 DI 範圍 (例如從不同的傳入要求) 觀察到,因為它們共用相同的處理常式。
請考慮採用下列建議,協助減輕此已知限制:
❌ 請勿快取 HttpMessageHandler
執行個體或其相依性內的任何範圍相關資訊 (例如來自 HttpContext
的資料),以免洩露敏感資訊。
❌ 請勿使用 cookie,因為 CookieContainer
會與處理常式共用。
✔️ 建議不要儲存資訊,或僅在 HttpRequestMessage
執行個體內傳遞。
若要隨 HttpRequestMessage
一起傳遞任意資訊,您可以使用 HttpRequestMessage.Options 屬性。
✔️ 建議將所有範圍相關 (例如驗證) 邏輯封裝在不是由 IHttpClientFactory
建立的其他 DelegatingHandler
中,並用它來裝合 IHttpClientFactory
建立的處理常式。
若要建立不含 HttpClient
的 HttpMessageHandler
,請針對任何已註冊的具名用戶端呼叫 IHttpMessageHandlerFactory.CreateHandler。 在該情況下,您必須使用組合的處理常式自行建立 HttpClient
執行個體。 您可以在 GitHub 找到完全可執行的範例來對應此因應措施。
如需詳細資訊,請參閱 IHttpClientFactory
指導方針的 IHttpClientFactory 訊息處理常式範圍一節。
HttpClient
與 DNS 變更無關
即使已使用 IHttpClientFactory
,仍可能遇到 DNS 過時問題。 如果 Singleton
服務擷取到 HttpClient
執行個體,或廣泛而言,執行個體儲存在某處的時間超過指定的 HandlerLifetime
一段時間,這種情況通常就會發生。 如果單一資料庫擷取到對應的具型別用戶端,系統也會擷取到 HttpClient
。
❌ 請勿長時間快取 IHttpClientFactory
建立的 HttpClient
執行個體。
❌ 請勿將具型別的用戶端執行個體插入 Singleton
服務。
✔️ 建議及時或每次需要時都請求 IHttpClientFactory
的用戶端。 原廠建立的用戶端可以安全處置。
IHttpClientFactory
所建立的 HttpClient
執行個體預計為「短期」。
HttpMessageHandler
的存留期到期時將其回收和重新建立是IHttpClientFactory
的必要動作,以確保處理常式回應 DNS 變更。HttpClient
會在建立時繫結至特定處理常式執行個體,因此應該及時要求新的HttpClient
執行個體,以確保用戶端將取得更新過的處理常式。處置這類原廠建立的
HttpClient
執行個體,並不會導致通訊端耗盡,因為這類處置不會觸發HttpMessageHandler
的處置。IHttpClientFactory
會追蹤並處置用來建立HttpClient
執行個體 (特別是HttpMessageHandler
執行個體) 的資源,只要這些執行個體的存留期到期,而且HttpClient
不再予以使用。
具型別的用戶端的存留期也預設較短,因為 HttpClient
執行個體已插入建構函式,因此會共用具型別用戶端的存留期。
如需詳細資訊,請參閱 IHttpClientFactory
指導方針中的HttpClient
存留期管理和避免在單一服務使用具型別的用戶端章節。
HttpClient
使用過多通訊端
即使已使用 IHttpClientFactory
,在特定使用案例仍可能遇到通訊端耗盡的問題。 根據預設,HttpClient
不會限制並行要求的數量。 如果同時啟動大量 HTTP/1.1 請求,由於集區中沒有免費連線,且沒有設定限制,每個請求最終都會觸發新的 HTTP 連線嘗試。
❌ 請勿在同時啟動大量 HTTP/1.1 請求時,不指定限制。
✔️ 建議將 HttpClientHandler.MaxConnectionsPerServer (如果當作主要處理常式來使用時則為 SocketsHttpHandler.MaxConnectionsPerServer) 設定為合理的值。 請注意,這些限制僅適用於特定處理常式執行個體。
✔️ 建議使用 HTTP/2,以便允許透過單一 TCP 連線進行多工請求。
具型別的用戶端插入錯誤的 HttpClient
在多種情況下,插入具型別用戶端的 HttpClient
可能並非預期。 大部分時候,根本原因在於組態錯誤,因為根據 DI 設計,服務的任何後續註冊都會覆寫先前的註冊。
具型別的用戶端會「在背後」使用具名用戶端:新增具型別的用戶端會以隱含方式將它註冊並連結至具名用戶端。 除非明確提供,否則用戶端名稱將會設為 TClient
的型別名稱。 如果使用 AddHttpClient<TClient,TImplementation>
多載,該名稱就會是 TClient,TImplementation
組合中的第一個。
因此,註冊具型別的用戶端會執行兩個不同的動作:
- 註冊具名用戶端 (舉簡單的預設例子,名稱為
typeof(TClient).Name
)。 - 使用既有的
TClient
或TClient,TImplementation
註冊Transient
服務。
下列兩個敘述原則上是一樣的:
services.AddHttpClient<ExampleClient>(c => c.BaseAddress = new Uri("http://example.com"));
// -OR-
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")) // register named client
.AddTypedClient<ExampleClient>(); // link the named client to a typed client
在簡單的例子中,它也與下列內容相似:
services.AddHttpClient(nameof(ExampleClient), c => c.BaseAddress = new Uri("http://example.com")); // register named client
// register plain Transient service and link it to the named client
services.AddTransient<ExampleClient>(s =>
new ExampleClient(
s.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(ExampleClient))));
請考慮採用下列範例,瞭解如何中斷具型別和具名的用戶端之間的連結。
具型別的用戶端已註冊第二次
❌ 請勿另行註冊具型別的用戶端,AddHttpClient<T>
呼叫已自動進行註冊。
如果誤將具型別的用戶端註冊為單純的暫時性服務兩次,它會覆寫 HttpClientFactory
新增的註冊,進而中斷具名用戶端的連結。 它的顯示方式會假定 HttpClient
組態遺失,因為插入具型別的用戶端的會改為未設定的 HttpClient
。
相比擲回例外狀況,使用「錯誤」的 HttpClient
可能會造成混淆。 發生這種情況是因為,「預設」未設定的 HttpClient
(也就是具有 Options.DefaultName 名稱的用戶端 (string.Empty
)) 已註冊為單純的暫時性服務,藉此啟用最基本的 HttpClientFactory
使用案例。 因此,在連結中斷且具型別的用戶端成為一般服務之後,這個「預設」的 HttpClient
會自然插入對應的建構函式參數。
不同的具型別用戶端會註冊在通用的介面上
如果有兩個不同的具型別用戶端在通用介面上註冊,兩者都會重複使用相同的具名用戶端。 這種狀況可看作第一個具型別的用戶端「誤」將第二個具名用戶端插入。
❌ 請勿在單一介面註冊多個具型別的用戶端時,不明確指定名稱。
✔️ 建議分別註冊和設定具名用戶端,然後將它連結至一或多個具型別用戶端,方法是在 AddHttpClient<T>
呼叫中指定名稱,或在設定具名用戶端時呼叫 AddTypedClient
。
根據設計,註冊並設定具有相同名稱的具名用戶端多次,只會將組態動作附加至現有用戶端清單。 這種 HttpClientFactory
的行為可能並不明顯,但它與選項模式及 Configure 這類組態 API 所使用的方法相同。
這種方法最適用於進階處理常式組態,例如將自定義處理常式新增至外部定義的具名用戶端,或模擬測試用的主要處理常式,但它也適用於 HttpClient
執行個體組態。 舉例來說,下列三個範例會使系統以相同方式設定 HttpClient
(BaseAddress
和 DefaultRequestHeaders
都會完成設定):
// one configuration callback
services.AddHttpClient("example", c =>
{
c.BaseAddress = new Uri("http://example.com");
c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0");
});
// -OR-
// two configuration callbacks
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"))
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
// -OR-
// two configuration callbacks in separate AddHttpClient calls
services.AddHttpClient("example", c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient("example")
.ConfigureHttpClient(c => c.DefaultRequestHeaders.UserAgent.ParseAdd("HttpClient/8.0"));
這能讓具型別用戶端連結到已定義的具名用戶端,以及將數個具型別用戶端連結至單一具名用戶端。 使用具有 name
參數的多載時更明顯:
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));
services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");
在設定AddTypedClient具名用戶端時呼叫 ,也可以達成相同的結果:
services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
.AddTypedClient<FooLogger>()
.AddTypedClient<BarLogger>();
不過,如果您不想重複使用相同的具名用戶端,但仍想要在相同的介面註冊用戶端,您可以為它們明確指定不同名稱來完成這件事:
services.AddHttpClient<ITypedClient, ExampleClient>(nameof(ExampleClient),
c => c.BaseAddress = new Uri("http://example.com"));
services.AddHttpClient<ITypedClient, GithubClient>(nameof(GithubClient),
c => c.BaseAddress = new Uri("https://github.com"));