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 queIHttpClientFactory
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 instancesHttpClient
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 deHttpMessageHandler
.IHttpClientFactory
effectue le suivi et l’élimination des ressources utilisées pour créer des instancesHttpClient
, en particulier les instancesHttpMessageHandler
, dès que leur durée de vie expire et qu’il n’y a plus deHttpClient
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 HttpClient
gestion 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 :
- Il enregistre un client nommé (dans un cas simple par défaut, le nom est
typeof(TClient).Name
). - Il enregistre un service
Transient
utilisant leTClient
ou leTClient,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"));