Partager via


Problèmes courants d’utilisation de IHttpClientFactory

Dans cet article, vous découvrirez certains des problèmes les plus courants auxquels vous pouvez être confronté lorsque vous utilisez IHttpClientFactory pour créer des instances HttpClient.

IHttpClientFactory est un moyen pratique de configurer plusieurs configurations HttpClient dans le conteneur d’injection de dépendances (DI), de configurer la journalisation, de mettre en place des stratégies de résilience, et plus encore. IHttpClientFactory encapsule également la gestion de la durée de vie des instances, afin d’éviter HttpClient HttpMessageHandler des problèmes tels que l’épuisement des sockets et la perte de modifications DNS. Pour obtenir un aperçu de l’utilisation de IHttpClientFactory dans votre application .NET, veuillez consulter la section IHttpClientFactory avec .NET.

En raison de la nature complexe de l’intégration de IHttpClientFactory avec DI, vous pouvez rencontrer certains problèmes qui peuvent être difficiles à détecter et à résoudre. Les scénarios répertoriés dans cet article contiennent également des recommandations que vous pouvez appliquer de manière proactive pour éviter d’éventuels problèmes.

HttpClient ne respecte pas la durée de vie de Scoped

Vous pouvez rencontrer un problème si vous avez besoin d’accéder à un service à portée, par exemple HttpContext, ou à un cache à portée, depuis HttpMessageHandler. Les données enregistrées peuvent soit « disparaître », soit, à l’inverse, « persister » alors qu’elles ne le devraient pas. Cela est causé par un décalage de portée de l’injection de dépendances (DI) entre le contexte de l’application et l’instance du gestionnaire, et c’est une limitation connue dans IHttpClientFactory.

IHttpClientFactory crée une étendue d’ID distincte pour chaque instance HttpMessageHandler. Ces étendues de gestionnaire sont distinctes des portées de contexte d’application (par exemple, la portée de la requête entrante dans ASP.NET Core, ou une portée DI manuelle créée par l’utilisateur), de sorte qu’elles ne partageront pas les instances de services à portée.

En conséquence de cette limitation :

  • Toutes les données mises en cache « à l’extérieur » dans un service à portée ne seront pas disponibles dans le HttpMessageHandler.
  • Toutes les données mises en cache « à l’intérieur » du HttpMessageHandler ou de ses dépendances à portée peuvent être observées depuis plusieurs portées DI d’application (par exemple, depuis différentes requêtes entrantes), car elles peuvent partager le même gestionnaire.

Considérez les recommandations suivantes pour atténuer cette limitation connue :

❌ NE mettez PAS en cache des informations liées à la portée (comme des données de HttpContext) à l’intérieur des instances HttpMessageHandler ou de leurs dépendances pour éviter de divulguer des informations sensibles.

❌ NE PAS utiliser de cookies, car le CookieContainer sera partagé avec le gestionnaire.

✔️ ENVISAGEZ de ne pas stocker les informations, ou de les transmettre uniquement via l’instance HttpRequestMessage.

Pour transmettre des informations arbitraires en plus de HttpRequestMessage, vous pouvez utiliser la propriété HttpRequestMessage.Options.

✔️ ENVISAGEZ d’encapsuler toute la logique liée à la portée (par exemple, l’authentification) dans un DelegatingHandler distinct qui n’est pas créé par IHttpClientFactory, et utilisez-le pour envelopper le gestionnaire créé par IHttpClientFactory.

Pour créer simplement un HttpMessageHandler sans HttpClient, appelez IHttpMessageHandlerFactory.CreateHandler pour tout client nommé enregistré. Dans ce cas, vous devrez créer vous-même une instance HttpClient à l’aide du gestionnaire combiné. Vous pouvez trouver un exemple entièrement exécutable de cette solution de contournement sur GitHub.

Pour plus d’informations, consultez la section Portées du gestionnaire de messages dans IHttpClientFactory des IHttpClientFactory lignes directrices.

HttpClient ne respecte pas les changements DNS

Même si vous utilisez IHttpClientFactory, il est toujours possible de rencontrer le problème du DNS obsolète. Cela peut généralement se produire si une instance HttpClient est capturée dans un service Singleton, ou, en général, stockée quelque part pendant une période plus longue que le HandlerLifetime spécifié. HttpClient sera également capturé si le client typed respectif est capturé par un singleton.

❌ NE PAS mettre en cache les instances HttpClient créées par IHttpClientFactory pendant de longues périodes.

❌ NE PAS injecter d’instances de typed client dans les services Singleton singleton.

✔️ ENVISAGEZ de demander un client depuis IHttpClientFactory en temps opportun ou chaque fois que vous en avez besoin. Les clients créés par l’usine peuvent être éliminés sans risque.

Les instances HttpClient créées par IHttpClientFactory sont destinées à être de courte durée.

  • Le recyclage et la recréation des HttpMessageHandler à l’expiration de leur durée de vie sont essentiels pour que IHttpClientFactory puisse garantir que les gestionnaires réagissent aux modifications DNS. HttpClient étant lié à une instance de gestionnaire spécifique lors de sa création, de nouvelles instances HttpClient doivent être demandées en temps opportun pour garantir que le client obtiendra le gestionnaire mis à jour.

  • Éliminer de telles instances HttpClient créées par l’usine ne conduira pas à l’épuisement des sockets, car leur élimination ne déclenchera pas l’élimination de HttpMessageHandler. IHttpClientFactory effectue le suivi et l’élimination des ressources utilisées pour créer des instances HttpClient, en particulier les instances HttpMessageHandler, dès que leur durée de vie expire et qu’il n’y a plus de HttpClient les utilisant.

Les clients typés sont également destinés à être de courte durée, car une instance de HttpClient est injectée dans le constructeur, de sorte qu’elle partagera la durée de vie du typed client.

Pour plus d’informations, consultez la section HttpClientgestion de la durée de vie et Éviter les clients typés dans les services singleton dans les lignes directrices IHttpClientFactory.

HttpClient utilise trop de sockets

Même si IHttpClientFactory est utilisé, il est toujours possible de rencontrer un problème d’épuisement des sockets dans un scénario d’utilisation spécifique. Par défaut, HttpClient ne limite pas le nombre de requêtes simultanées. Si un grand nombre de requêtes HTTP/1.1 sont lancées simultanément, chacune d’entre elles finira par déclencher une nouvelle tentative de connexion HTTP, car il n’y a pas de connexion libre dans le pool et aucune limite n’est fixée.

❌ NE PAS démarrer un grand nombre de requêtes HTTP/1.1 simultanément sans spécifier de limites.

✔️ ENVISAGEZ de définir HttpClientHandler.MaxConnectionsPerServer (ou SocketsHttpHandler.MaxConnectionsPerServer, si vous l’utilisez comme gestionnaire principal) à une valeur raisonnable. Notez que ces limites ne s’appliquent qu’à l’instance de gestionnaire spécifique.

✔️ ENVISAGEZ d’utiliser HTTP/2, qui permet le multiplexage des requêtes sur une seule connexion TCP.

Le client typé a le mauvais HttpClient injecté

Il peut y avoir diverses situations où il est possible d’obtenir un HttpClient inattendu injecté dans un client typé. La plupart du temps, la cause racine sera une erreur de configuration, car, par conception DI, tout enregistrement ultérieur d’un service remplace le précédent.

Les clients typés utilisent des clients nommés « sous le capot » : l’ajout d’un client typé enregistre implicitement et le lie à un client nommé. Le nom du client, à moins qu’il ne soit explicitement fourni, sera défini comme le nom du type TClient. Ce sera le premier de la paire TClient,TImplementation si des surcharges AddHttpClient<TClient,TImplementation> sont utilisées.

Par conséquent, l’enregistrement d’un client typé fait deux choses distinctes :

  1. Il enregistre un client nommé (dans un cas simple par défaut, le nom est typeof(TClient).Name).
  2. Il enregistre un service Transient utilisant le TClient ou le TClient,TImplementation fourni.

Les deux déclarations suivantes sont techniquement les mêmes :

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

Dans un cas simple, cela sera également similaire à ce qui suit :

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

Considérez les exemples suivants de la manière dont le lien entre clients typés et nommés peut être rompu.

Le client typé est enregistré une deuxième fois

❌ NE PAS enregistrer le client typé séparément : il est déjà enregistré automatiquement par l’appel AddHttpClient<T>.

Si un client typé est enregistré par erreur une deuxième fois en tant que service Transient simple, cela remplacera l’enregistrement ajouté par le HttpClientFactory, rompant ainsi le lien avec le client nommé. Cela se manifestera comme si la configuration du HttpClient était perdue, car un HttpClient non configuré sera injecté dans le client typé à la place.

Cela peut prêter à confusion car, au lieu de lever une exception, un « mauvais » HttpClient est utilisé. Cela se produit parce que le HttpClient non configuré « par défaut », le client avec le nom Options.DefaultName (string.Empty), est enregistré en tant que service Transient simple, pour permettre le scénario d’utilisation HttpClientFactory le plus basique. C’est pourquoi, après que le lien a été rompu et que le client typé devient un service ordinaire, ce HttpClient « par défaut » sera naturellement injecté dans le paramètre de constructeur respectif.

Différents clients typés sont enregistrés sur une interface commune

Dans le cas où deux clients typés différents sont enregistrés sur une interface commune, ils réutiliseraient tous deux le même client nommé. Cela peut donner l’impression que le premier client typé obtient le deuxième client nommé injecté « par erreur ».

❌ NE PAS enregistrer plusieurs clients typés sur une interface unique sans spécifier explicitement le nom.

✔️ ENVISAGEZ d’enregistrer et de configurer un client nommé séparément, puis de le lier à un ou plusieurs clients typés, soit en spécifiant le nom dans l’appel AddHttpClient<T>, soit en appelant AddTypedClient lors de la configuration du client nommé.

Par conception, l’enregistrement et la configuration d’un client nommé avec le même nom plusieurs fois ajoutent simplement les actions de configuration à la liste des existantes. Ce comportement de HttpClientFactory peut ne pas être évident, mais il s’agit de la même approche que celle utilisée par le modèle Options et les API de configuration telles que Configure.

Cela est principalement utile pour des configurations avancées de gestionnaire, par exemple, l’ajout d’un gestionnaire personnalisé à un client nommé défini en externe, ou la simulation d’un gestionnaire principal pour des tests, mais cela fonctionne également pour la configuration d’instance HttpClient. Par exemple, les trois exemples suivants aboutiront à un HttpClient configuré de la même manière (les deux BaseAddress et DefaultRequestHeaders sont définis) :

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

Cela permet de lier un client typé à un client nommé déjà défini, et également de lier plusieurs clients typés à un seul client nommé. Cela est plus évident lorsque des surcharges avec un paramètre name sont utilisées :

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

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

La même chose peut également être réalisée en appelant AddTypedClient lors de la configuration du client nommé :

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

Cependant, si vous ne souhaitez pas réutiliser le même client nommé, mais que vous souhaitez toujours enregistrer les clients sur la même interface, vous pouvez le faire en spécifiant explicitement des noms différents pour eux :

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

Voir aussi