一般的なIHttpClientFactory使用に関する問題

この記事では、IHttpClientFactory を使用して HttpClient インスタンスを作成する際に発生する場合がある最も一般的な問題について説明します。

IHttpClientFactory は、DI コンテナで複数の HttpClient 構成を設定したり、ログ記録を構成したり、回復性戦略を設定したりするための便利な方法です。 IHttpClientFactory また、ソケットの枯渇や DNS の変更の損失などの問題を防ぐために、HttpClient インスタンスと HttpMessageHandler インスタンスの有効期間管理を取り込みます。 .NET アプリケーションで IHttpClientFactory を使用する方法の概要については、「.NET の IHttpClientFactory」を参照してください。

DI との IHttpClientFactory 統合の複雑な本質により、トラブルシューティングが難しい問題に直面する場合があります。 この記事に記載されているシナリオには、潜在的な問題を回避するために事前に適用できる推奨事項も含まれています。

HttpClientScoped ライフタイムを考慮しない

たとえば HttpMessageHandlerHttpContext などのスコープ付きサービス、スコープ付きキャッシュにアクセスする必要がある場合は、問題が発生する場合があります。 そこに保存されているデータは、「消える」場合があれば、逆に消えないはずのデータが「残る」こともあります。 これは、アプリケーション コンテキストとハンドラ インスタンスの間の依存性の注入 (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 は破棄されないためです。 IHttpClientFactoryHttpClient インスタンスの作成に使用されたリソース (具体的には 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 つの処理が実行されます。

  1. 名前付きクライアントを登録します (単純な既定のケースの場合、名前は typeof(TClient).Name)。
  2. 指定された 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 につながります (BaseAddressDefaultRequestHeaders の両方が設定されます)。

// 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"));

関連項目