Freigeben über


Häufige Probleme bei der Verwendung von IHttpClientFactory

In diesem Artikel lernen Sie einige der häufigsten Probleme kennen, die beim IHttpClientFactory Erstellen von HttpClient-Instanzen auftreten können.

IHttpClientFactory ist eine bequeme Möglichkeit, mehrere HttpClient Konfigurationen im DI-Container einzurichten, die Protokollierung zu konfigurieren, Resilienzstrategien einzurichten und vieles mehr. IHttpClientFactory kapselt auch die Lebensdauerverwaltung und HttpClient HttpMessageHandler Instanzen, um Probleme wie Socketausschöpfung und Verlust von DNS-Änderungen zu verhindern. Eine Übersicht zur Verwendung von IHttpClientFactory in Ihrer .NET-Anwendung finden Sie unter IHttpClientFactory mit .NET.

Aufgrund einer komplexen Art der IHttpClientFactory-Integration mit DI können einige Probleme auftreten, die möglicherweise schwer zu erfassen und zu beheben sind. Die in diesem Artikel aufgeführten Szenarien enthalten auch Empfehlungen, die Sie proaktiv anwenden können, um potenzielle Probleme zu vermeiden.

HttpClient respektiert Scoped die Lebensdauer nicht

Sie können ein Problem feststellen, wenn Sie auf einen beliebigen bereichsbezogenen Dienst, zum Beispiel HttpContext bzw. auf einen bereichsbezogenen Cache von HttpMessageHandler aus zugreifen müssen. Die dort gespeicherten Daten können entweder „verschwinden“ oder umgekehrt, „bestehen bleiben“, obwohl sie das nicht sollen. Dies wird durch den Dependency Injection (DI)-Bereichskonflikt zwischen dem Anwendungskontext und der Handlerinstanz verursacht, und es ist eine bekannte Einschränkung in IHttpClientFactory.

IHttpClientFactory erstellt für jede HttpMessageHandler-Instanz einen separaten DI-Bereich. Diese Handlerbereiche sind von Anwendungskontextbereichen getrennt (z. B. vom ASP.NET Core-Bereich für eingehende Anforderungen oder einem vom Benutzer erstellten manuellen DI-Bereich), sodass sie keine bereichsbezogenen Dienstinstanzen gemeinsam nutzen.

Auswirkungen durch diese Beschränkung:

  • Alle Daten, die in einem bereichsbezogenen Dienst „extern“ zwischengespeichert wurden, sind nicht innerhalb des HttpMessageHandler-Bereichs verfügbar.
  • Alle Daten, die innerhalb von HttpMessageHandler oder der bereichsbezogenen Abhängigkeiten zwischengespeichert werden, können aus mehreren Anwendungs-DI-Bereichen (z. B. aus unterschiedlichen eingehenden Anforderungen) beobachtet werden, da sie denselben Handler gemeinsam nutzen können.

Beachten Sie die folgenden Empfehlungen, um diese bekannte Einschränkung zu beheben:

❌ Zwischenspeichern Sie KEINE bereichsbezogenen Informationen (z. B. Daten von HttpContext) innerhalb derHttpMessageHandler-Instanzen oder deren Abhängigkeiten, um zu vermeiden, dass vertrauliche Informationen verloren gehen.

❌ Verwenden Sie KEINE Cookies, da CookieContainer gemeinsam mit dem Handler freigegeben wird.

✔️ ERWÄGEN Sie, die Informationen nicht zu speichern oder nur innerhalb der HttpRequestMessage-Instanz zu übergeben.

Um beliebige Informationen zusammen mit der HttpRequestMessage zu übergeben, können Sie die HttpRequestMessage.Options-Eigenschaft verwenden.

✔️ ERWÄGEN Sie, die gesamte bereichsbezogene Logik (z. B. Authentifizierung) in einem separaten DelegatingHandler, der nicht von IHttpClientFactory erstellt wurde, und verwenden Sie ihn, um den von IHttpClientFactory erstellten Handler zu verpacken.

Um nur einen HttpMessageHandler ohne HttpClient zu erstellen, rufen Sie IHttpMessageHandlerFactory.CreateHandler für einen beliebigen registrierten benannten Client auf. In diesem Fall müssen Sie eine HttpClient-Instanz selbst mithilfe des kombinierten Handlers erstellen. Sie finden ein vollständig ausgeführtes Beispiel für diese Problemumgehung auf GitHub.

Weitere Informationen finden Sie im Abschnitt Nachrichtenhandlerbereiche in IHttpClientFactory in den IHttpClientFactory-Richtlinien.

HttpClient berücksichtigt DNS-Änderungen nicht

Auch wenn IHttpClientFactory verwendet wird, ist es weiterhin möglich, dass das veraltete DNS-Problem auftritt. Dies kann in der Regel passieren, wenn eine HttpClient-Instanz in einem Singleton-Dienst erfasst wird oder im Allgemeinen für einen bestimmten Zeitraum gespeichert wird, der länger als der angegebene HandlerLifetime ist. HttpClient wird auch erfasst, wenn der jeweilige typisierte Client von einem Singleton erfasst wird.

❌ Zwischenspeichern Sie KEINE HttpClient-Instanzen, die von IHttpClientFactory erstellt wurden, für einen längeren Zeitraum.

❌ Injizieren Sie die Instanzen des typisierten Client NICHT in die Singleton-Dienste.

✔️ ERWÄGEN Sie, rechtzeitig oder jedes Mal, wenn Sie einen benötigen, einen Client aus IHttpClientFactory anzufordern. Vom Hersteller erstellte Clients können sicher verworfen werden.

HttpClient-Instanzen, die von IHttpClientFactory erstellt werden, sollen kurzlebig sein.

  • Das Recyceln und erneute Erstellen von HttpMessageHandlern nach Ablauf ihrer Lebensdauer ist für IHttpClientFactory unerlässlich, um sicherzustellen, dass Handler auf DNS-Änderungen reagieren. HttpClient ist bei der Erstellung an eine bestimmte Handlerinstanz gebunden, sodass neue HttpClient-Instanzen rechtzeitig angefordert werden sollten, um sicherzustellen, dass der Client den aktualisierten Handler erhält.

  • Die Entsorgung solcher HttpClient-Instanzen, die von der Factory erstellt wurden, führt nicht zu Socketerschöpfung, da ihre Entsorgung keine Entsorgung des HttpMessageHandler auslöst. IHttpClientFactory verfolgt Ressourcen nach und entsorgt diese, die zum Erstellen von HttpClient-Instanzen verwendet werden, insbesondere die HttpMessageHandler-Instanzen, sobald ihre Lebensdauer abläuft und sie nicht mehr von einem HttpClient verwendet werden.

Typisierte Clients sind auch nur für den kurzfristigen Einsatz gedacht, da eine HttpClient-Instanz in den Konstruktor eingefügt wird, sodass sie die Lebensdauer des typisierten Clients freigibt.

Weitere Informationen finden Sie unter HttpClientLebensdauerverwaltung und Vermeiden von typisierten Clients in Singleton-Diensten in den IHttpClientFactory-Richtlinien.

HttpClient verwendet zu viele Sockets

Selbst wenn IHttpClientFactory verwendet wird, ist es weiterhin möglich, das Problem der Socketerschöpfung mit einem bestimmten Verwendungsszenario zu erreichen. Beschränkt standardmäßig HttpClient nicht die Anzahl gleichzeitiger Anforderungen. Wenn eine große Anzahl von HTTP/1.1-Anforderungen gleichzeitig gestartet wird, löst jeder von ihnen einen neuen HTTP-Verbindungsversuch aus, da keine freie Verbindung im Pool besteht und kein Grenzwert festgelegt ist.

❌ Starten Sie KEINE große Anzahl von HTTP/1.1-Anforderungen gleichzeitig, ohne die Grenzwerte anzugeben.

✔️ ERWÄGEN Sie die Einstellung HttpClientHandler.MaxConnectionsPerServer (oder SocketsHttpHandler.MaxConnectionsPerServer, wenn Sie sie als primären Handler verwenden), um einen angemessenen Wert zu verwenden. Beachten Sie, dass diese Grenzwerte nur für die spezifische Handlerinstanz gelten.

✔️ ERWÄGEN Sie die Verwendung von HTTP/2, wodurch Multiplexing-Anforderungen über eine einzelne TCP-Verbindung möglich sind.

Der typisierte Client hat den falschen HttpClient Injiziert.

Es kann verschiedene Situationen geben, in denen ein unerwarteter HttpClient in einen typisierten Client injiziert werden kann. Meistens wird die Ursache in einer fehlerhaften Konfiguration auftreten, da jede nachfolgende Registrierung eines Diensts die vorherige außer Kraft setzt.

Typisierte Clients verwenden benannte Clients „unter der Haube“: durch das Hinzufügen eines typisierten Clients wird er impliziert registriert und mit einem benannten Client verknüpft. Der Clientname wird auf den Typnamen von TClient festgelegt, es sei denn er wurde explizit angegeben. Dies wäre der erste aus dem TClient,TImplementation-Paar, wenn die AddHttpClient<TClient,TImplementation>-Overloads verwendet werden.

Daher werden durch das Registrieren eines typisierten Clients zwei getrennte Aktionen ausgeführt:

  1. Registriert einen benannten Client (in einem einfachen Standardfall lautet der Nametypeof(TClient).Name).
  2. Registriert einen Transient-Dienst mithilfe von TClient oder des bereitgestellten TClient,TImplementation.

Die folgenden beiden Aussagen sind technisch identisch:

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

In einem einfachen Fall ähnelt dies dem folgenden Fall:

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

Sehen Sie sich die folgenden Beispiele an, wie die Verknüpfung zwischen einem typisierten und einem benannten Client unterbrochen werden kann.

Der typisierte Client wird ein zweites Mal registriert

❌ Registrieren Sie den typisierten Client NICHT separat – er wird bereits automatisch durch den AddHttpClient<T>-Aufruf registriert.

Wenn ein typisierter Client fälschlicherweise ein zweites Mal als einfacher Transient-Dienst registriert wird, überschreibt dies die durch den HttpClientFactorybenannten Client hinzugefügte Registrierung, und die Verknüpfung mit dem benannten Client wird aufgehoben. Es manifestiert sich so, als ob die Konfiguration von HttpClient verloren geht, da ein nicht konfigurierter HttpClient stattdessen in den typisierten Client eingefügt wird.

Es kann verwirrend sein, dass anstelle einer Ausnahme ein „falscher“ HttpClient verwendet wird. Dies geschieht, da der nicht konfigurierte „standardmäßige“ HttpClient – der Client mit dem Namen Options.DefaultName (string.Empty) – als einfacher Transient-Dienst registriert ist, um das einfachste HttpClientFactory-Verwendungsszenario zu aktivieren. Nach die Verknüpfung aufgehoben wird und der typisierte Client zu einem gewöhnlichen Dienst wird, wird dieser „standardmäßige“ HttpClient natürlich in den entsprechenden Konstruktorparameter eingefügt.

Verschiedene typisierte Clients werden auf einer gemeinsamen Schnittstelle registriert.

Wenn zwei verschiedene typisierte Clients auf einer gemeinsamen Schnittstelle registriert sind, würden beide denselben benannten Client wiederverwenden. Dies kann wie der erste typisierte Client aussehen, der den zweiten benannten Client „falsch“ injiziert hat.

❌ Registrieren Sie NICHT mehrere typisierte Clients auf einer einzigen Schnittstelle, ohne den Namen explizit anzugeben.

✔️ ERWÄGEN Sie, einen benannten Client separat zu registrieren und zu konfigurieren, und verknüpfen Sie ihn dann mit einem oder mehreren typisierten Clients, indem Sie entweder den Namen im AddHttpClient<T> Aufruf oder durch das Aufrufen AddTypedClient während der Einrichtung des benannten Clients angeben.

Wenn Sie einen benannten Client mit demselben Namen mehrmals registrieren, werden die Konfigurationsaktionen automatisch an die Liste der vorhandenen Aktionen angehängt. Dieses Verhalten von HttpClientFactory ist möglicherweise nicht offensichtlich, aber es ist derselbe Ansatz, der von den Optionsmustern und Konfigurations-APIs, wie Configure verwendet wird.

Dies ist hauptsächlich nützlich für erweiterte Handlerkonfigurationen, z. B. das Hinzufügen eines benutzerdefinierten Handlers zu einem benannten Client, der extern definiert wurde, oder das Modell eines primären Handlers für Tests, funktioniert aber auch für die HttpClient-Instanzkonfiguration. Die drei folgenden Beispiele führen beispielsweise zu einem HttpClient, der auf die gleiche Weise (sowohl BaseAddress als auch DefaultRequestHeaders sind festgelegt) konfiguriert wurde:

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

Dies ermöglicht das Verknüpfen eines typisierten Clients mit einem bereits definierten benannten Client und auch das Verknüpfen mehrerer typisierter Clients mit einem einzelnen benannten Client. Es ist offensichtlicher, wenn Overloads mit einem name-Parameter verwendet werden:

services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress));

services.AddHttpClient<FooLogger>("LogClient");
services.AddHttpClient<BarLogger>("LogClient");

Dasselbe kann auch durch Aufrufen von AddTypedClient während der Konfiguration des benannten Clients erreicht werden:

services.AddHttpClient("LogClient", c => c.BaseAddress = new Uri(LogServerAddress))
    .AddTypedClient<FooLogger>()
    .AddTypedClient<BarLogger>();

Wenn Sie jedoch nicht denselben benannten Client wiederverwenden möchten, Sie aber dennoch die Clients auf derselben Schnittstelle registrieren möchten, können Sie dies tun, indem Sie explizit unterschiedliche Namen dafür angeben:

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

Siehe auch