一般的なIHttpClientFactory
使用に関する問題
この記事では、IHttpClientFactory
を使用して HttpClient
インスタンスを作成する際に発生する場合がある最も一般的な問題について説明します。
IHttpClientFactory
は、DI コンテナで複数の HttpClient
構成を設定したり、ログ記録を構成したり、回復性戦略を設定したりするための便利な方法です。 IHttpClientFactory
また、ソケットの枯渇や DNS の変更の損失などの問題を防ぐために、HttpClient
インスタンスと HttpMessageHandler
インスタンスの有効期間管理を取り込みます。 .NET アプリケーションで IHttpClientFactory
を使用する方法の概要については、「.NET の IHttpClientFactory」を参照してください。
DI との IHttpClientFactory
統合の複雑な本質により、トラブルシューティングが難しい問題に直面する場合があります。 この記事に記載されているシナリオには、潜在的な問題を回避するために事前に適用できる推奨事項も含まれています。
HttpClient
が Scoped
ライフタイムを考慮しない
たとえば HttpMessageHandler
の HttpContext
などのスコープ付きサービス、スコープ付きキャッシュにアクセスする必要がある場合は、問題が発生する場合があります。 そこに保存されているデータは、「消える」場合があれば、逆に消えないはずのデータが「残る」こともあります。 これは、アプリケーション コンテキストとハンドラ インスタンスの間の依存性の注入 (DI) スコープの不一致 によって発生し、IHttpClientFactory
の既知の制限事項です。
IHttpClientFactory
は、各 HttpMessageHandler
インスタンスごとに個別の DI スコープを作成します。 これらのハンドラ スコープは、アプリケーション コンテキスト スコープ (たとえば、ASP.NET Core 受信要求スコープやユーザーが作成した手動 DI スコープなど) とは区別されるため、スコープ付きサービス インスタンスは共有されません。
この制限の結果、以下のようになります。
- スコープ付きサービスで「外部」でキャッシュされるデータは、
HttpMessageHandler
では利用できません。 HttpMessageHandler
またはそのスコープ付き依存性内で「内部」でキャッシュされたデータは、同じハンドラを共有できるため、複数のアプリケーション DI スコープ (例: 別の受信要求から) で、確認できます。
この既知の制限を軽減するには、次の推奨事項を検討してください。
❌機密情報の漏洩を防ぐため、HttpMessageHandler
インスタンスまたはその依存性内にあるスコープ関連の情報 (例: HttpContext
からのデータなど) をキャッシュしないでください。
❌ CookieContainer
はハンドラと一緒に共有されるため、Cookie を使用しないでください。
✔️ 情報を格納しないか、HttpRequestMessage
インスタンス内でのみ渡すかを検討してください。
HttpRequestMessage
と共に任意の情報を渡す場合は、HttpRequestMessage.Options プロパティを使用できます。
✔️ すべてのスコープ関連ロジック (例: 認証) を、IHttpClientFactory
以外が作成した別の DelegatingHandler
でカプセル化し、それをIHttpClientFactory
が作成したハンドラにラップするために使用することを検討してください。
HttpClient
なしで HttpMessageHandler
のみを作成するには、登録されている名前付きクライアントに対して IHttpMessageHandlerFactory.CreateHandler を呼び出します。 その場合、組み合わされたハンドラーを使用して自分で HttpClient
インスタンスを作成します。 この回避策の完全に実行可能な例は、GitHub にあります。
詳細については、IHttpClientFactory
ガイドラインの「IHttpClientFactory のメッセージ ハンドラ スコープ」セクションを参照してください。
HttpClient
が DNS の変更を考慮しない
IHttpClientFactory
が使用されていても、古い DNS 問題が発生する場合があります。 これは通常、HttpClient
インスタンスが Singleton
サービスでキャプチャされた場合、または、一般的に指定された HandlerLifetime
より長い期間どこかに保管された場合に起こります。 HttpClient
は、各 型指定クライアントがシングルトンによってキャプチャされた際にも、キャプチャされます。
❌ IHttpClientFactory
が作成した HttpClient
インスタンスを長期間キャッシュしないでください。
❌ 型指定クライアントインスタンスを Singleton
サービスに注入しないでください。
✔️ クライアントをタイムリーに、または必要に応じて、IHttpClientFactory
で要求することを検討してください。 ファクトリで作成されたクライアントは、安全に破棄できます。
IHttpClientFactory
によって作成される HttpClient
インスタンスは、短い有効期間であることが想定されます。
HttpMessageHandler
の有効期限が切れたときにそのリサイクルと再作成を行うことは、IHttpClientFactory
にとってハンドラーが DNS の変更に対応できるようにするために不可欠です。HttpClient
は作成時に特定のハンドラー インスタンスに関連付けられるため、新しいHttpClient
インスタンスを適切なタイミングで要求し、クライアントが更新されたハンドラーを確実に取得できるようにする必要があります。ファクトリによって作成されたこのような
HttpClient
インスタンスを破棄してもソケットは枯渇しません。破棄してもHttpMessageHandler
は破棄されないためです。IHttpClientFactory
はHttpClient
インスタンスの作成に使用されたリソース (具体的にはHttpMessageHandler
インスタンス) を追跡し、破棄します。それらの有効期間はすぐに期限切れになり、それらを使うHttpClient
がなくなるからです。
型指定クライアント は、short-lived を目的としており HttpClient
インスタンスがコンストラクタに挿入されるため、型指定クライアントライフタイムが共有されます。
詳細については、IHttpClientFactory
ガイドラインの、HttpClient
「ライフタイム管理」および「シングルトン サービスの型指定クライアント」セクションを参照してください。
HttpClient
使用するソケットが多すぎる
IHttpClientFactory
が使用されていても、一部の使用シナリオでは、ソケットの枯渇問題が発生する場合があります。 既定では、HttpClient
は同時要求数を制限しません。 多数の HTTP/1.1 要求が同時に開始された場合、プールに空き接続がなく、制限が設定されていないため、各要求で新しい HTTP 接続試行がトリガーされます。
❌ 制限を指定せずに、多数の HTTP/1.1 要求を同時に開始しないでください。
✔️ 適切な値 HttpClientHandler.MaxConnectionsPerServer (プライマリ ハンドラとして使用する場合は SocketsHttpHandler.MaxConnectionsPerServer) を設定することを検討してください。 これらの制限は、特定のハンドラ インスタンスにのみ適用されることに注意してください。
✔️ HTTP/2 を使用することを検討してください。これにより、1 つの TCP 接続で要求を多重化できます。
型指定ライアント に間違って挿入された HttpClient
がある
型指定クライアントに予期されずに HttpClient
が挿入される状況はたくさんあります。 ほとんどの場合、DI 設計では、後続のサービスの登録が前のサービスよりも優先されるため、根本原因として誤った構成が挙げられます。
型指定クライアントは、名前付きクライアントを「内部」で使用します。型指定クライアントを暗黙的に追加すると、名前付きクライアントに登録およびリンクされます。 明示的に指定されない限り、クライアント名は、TClient
のタイプ名になります。 これは、AddHttpClient<TClient,TImplementation>
オーバーロードが使用されている場合、TClient,TImplementation
ペアからの最初のものとなります。
したがって、型指定クライアントを登録すると次の 2 つの処理が実行されます。
- 名前付きクライアントを登録します (単純な既定のケースの場合、名前は
typeof(TClient).Name
)。 - 指定された
TClient
またはTClient,TImplementation
を使用してTransient
サービスを登録します。
次の 2 つのステートメントは、技術的には同じです。
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>
呼び出しによってすでに自動登録されています。
型指定クライアントがプレーンな Transient サービスとして誤って再度登録された場合、HttpClientFactory
が追加した登録がオーバーライドされ、名前付きクライアントのリンクが破損します。 未構成の HttpClient
が型指定クライアントに挿入されるため、HttpClient
の構成が失われたかのように表示されます。
例外をスローする代わりに、「間違った」HttpClient
を使用したと誤認する場合があります。 これは、Options.DefaultName の名前が付いた (string.Empty
) クライアントである「既定」の HttpClient
がプレーン Transient サービスとして登録され、最も基本的な HttpClientFactory
使用シナリオが可能になるためです。 そのため、リンクが破損して、型指定クライアントが通常のサービスになった後、この「既定」の HttpClient
は、それぞれのコンストラクタ パラメータに自然に挿入されます。
異なる型指定クライアントが共通インターフェイスに登録されている
2 つの異なる型指定クライアントが共通インターフェイスに登録されている場合、両方とも同じ名前付きクライアントを再利用します。 これは、最初の型指定クライアントが 2番目の名前付きクライアントを、「間違って」挿入したかのように認識されることがあります。
❌名前を明示的に指定せずに複数の型指定クライアントを 1 つのインターフェイスに登録しないでください。
✔️ 名前付きクライアントを別々に登録して構成し、それをAddHttpClient<T>
呼び出しで名前を指定するか、名前付きクライアント のセットアップ中に AddTypedClient
を呼び出すことで、1 つ以上の型指定クライアントにリンクします。
設計上、名付きクライアントを、同じ名前で複数回、登録して構成すると、既存のクライアント一覧に構成作業が付加されます。 この HttpClientFactory
の動作は、明白ではない場合がありますが、Options パターンや Configure などの構成 API が使用するアプローチと同じアプローチです。
これは、カスタム ハンドラを、外部で定義した名付きクライアントに追加したり、テスト用のプライマリ ハンドラをモッキングしたりするなど、高度なハンドラ構成に対して主に役立ちますが、HttpClient
インスタンス構成でも機能します。 たとえば、次の 3 つの例では、同じ方法で構成された 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"));
これにより、型指定クライアントを既に定義されている名前付きクライアントにリンクしたり、複数の型指定クライアントを 1 つの名前付きクライアントにリンクしたりできます。 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"));
関連項目
.NET