Aaaah les HttpClients. Il y a de nombreuses raisons de les utiliser, et ce peu importe le langage. Il suffit d’avoir besoin de contacter une ressource sur Internet – que ce soit une page Web ou une API – et paf ! On passe par les HttpClients.
Pourtant, bien qu’il s’agisse d’un fondamental dans la librairie .NET, c’est également un sujet rempli de pièges, et il est très facile de mal les utiliser. Surtout dans les Azure Functions.
Mais pas de panique ! MiddleWay à la rescousse ! Je vais vous détailler comment bien les utiliser ! Mais avant… une petite explication sur pourquoi il est si facile de mal s’en servir !
HttpClient est une surcouche au dessus d’un gestionnaire de message. Le fameux… HttpMessageHandler. On peut le voir dans les surcharges du constructeur de HttpClient :
Le piège est là, justement. Avec le constructeur par défaut, eh bien, HttpClient va créer un nouvel handler. A chaque fois. Ouaip, à chaque fois. Et, lorsque l’instance sera détruite, le handler sera détruit…. mais pas totalement. En vérité, ce handler va garder ses ports TCP ouverts, juste au cas où il reste des paquets HTTP à recevoir !
Il va le garder ouvert pendant un temps configurable, qui est, par défaut… infini. Oui oui, infini ! Les ports resteront ouverts jusqu’à ce que l’application soit entièrement redémarrée. C’est même écrit dans la doc, regardez :
Dans le cas d’une application où un seul client est créé, comme une application en one-shot, c’est OK car le risque est faible. Quand l’application redémarrera, les ports seront fermés automatiquement.
Mais qu’en est-il d’un projet où il y a BEAUCOUP de requêtes ? Ou qui doit tourner 24 heures sur 24 ?
Vous l’avez dans le mille. On va ouvrir des ports, on va les garder … et, au bout d’un moment, il n’y aura plus de port disponible, on ne pourra plus créer de HttpClient. C’est ce qu’on appelle le Port Exhaustion.
Le flux va finir en erreur car on n’a plus de port disponible. Dans le cas d’Azure, c’est encore pire ! Les ports ne sont pas au niveau de la Function App … mais au niveau de l’App Service Plan. Un App Service Plan en Port Exhaustion … c’est touuuutes les applications dessus qui tombent si elles utilisent des ports ! Une situation qu’on souhaiterait bien s’éviter …
Il existe beaucoup de documentations qui en parlent. Microsoft a même rédigé un guide sur le sujet
Et je vais vous la simplifier dans ce blog !
On pourrait se dire: « Baaaah ok, sinon je crée un seul client et hop je le réutilise encore et encore » et … oui ! C’est une manière de résoudre le problème. Dans ce cas, nous n’aurons plus de risque de saturer les ports, car il n’y aura qu’un seul handler.
Cependant, le handler repose sur des ressources qui finissent par expirer. Par exemple, les baux DNS ! Un problème bien connu, c’est de recevoir une erreur: « L’hôte est inconnu » alors que l’hôte existe bel et bien ! C’est juste parce que le handler n’a plus de bail DNS valide et doit être recyclé.
C’est pour ça qu’il est possible de passer au client un handler, qui lui peut être ajusté, surtout avec la propriété PooledConnectionLifetime. La régler à, par exemple, 15 minutes corrige définitivement le problème. Les ports ne resteront actifs que 15 minutes, maximum, et au delà, le prochain appel HTTP va recycler le handler, pour qu’il soit fonctionnel à nouveau.
internal static readonly HttpClient HTTP_CLIENT = new HttpClient(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(15) });
Cette solution fonctionne très bien dans les projets .NET tels que les applications consoles. Là où l’injection de dépendance n’est pas souvent mise en oeuvre.
Eh bien… sur Azure, les Azure Functions reposent intégralement sur l’injection de dépendance ! Et il s’avère que, dans ce genre de contexte, Microsoft a prévu le coup, et la bonne utilisation d’un HttpClient est très simple à mettre en oeuvre. On passe par …
Dans le cas d’une Azure Functions, IHttpClientFactory est disponible par défaut, et peut déjà être utilisé tel quel !
Il suffit de utiliser l’injection de dépendance comme n’importe quel autre service pour pouvoir s’en servir.
Vous pouvez l’utiliser en l’ajoutant dans n’importe quel constructeur et hop, il peut être utilisé pour obtenir un client ! Plus aucun risque de port exhaustion cette fois, car l’intégralité de la gestion de vie du handler est gérée par la librairie !
Notez, vous n’avez même pas besoin de dispose le client généré par la factory (avec la directive using). Si on regarde le code de Dispose de HttpClient … tout ce qu’il va essayer de faire c’est de dispose le handler si on lui a dit de le faire ! Mais IHttpClientFactory indique aux clients de ne jamais le faire ! On peut le voir dans le code source .NET directement, qui est open source.
On peut aussi injecter un HttpClient directement ! Mais du coup … quand utiliser l’un ou autre?
Tout dépend du lifetime du service qui l’appelle. Pour faire simple, à partir du moment où le service est Singleton, on n’a pas le droit d’injecter HttpClient. Car si on l’injecte dans un service Singleton, eh bien le client ne sera jamais rafraîchit. Il viendra un moment où son handler ne va plus avoir de bail DNS valide, et la factory ne sera pas en mesure de fournir un nouveau handler pour ce client-là !
C’est pour ça qu’ici on préfère utiliser IHttpClientFactory pour créer un nouveau client à chaque fois qu’on en a besoin. Aucun risque que le bail expire, donc. Sinon, par essence, Transcient et Scoped vont recréer le service à chaque appel, et donc… on aura un nouveau HttpClient. Aucun risque que le bail expire ici aussi pour ces deux lifetimes.
Pour faire simple:
Pour info, les Azure Functions sont Transcient. Si vous avez besoin de récupérer un client directement dans l’Azure Functions, vous pouvez injecter HttpClient directement.
Cette règle fonctionne également sur beaucoup de librairies Microsoft, telles que la librairie CosmosDB. Le CosmosClient a été prévu pour fonctionner en Singleton, et l’injecter en Transcient va causer une port exhaustion !
HttpClient est très pratique, mais c’est aussi une source de problèmes si on ne s’en sert pas correctement. Heureusement, le framework nous simplifie la vie.
L’usage de IHttpClientFactory dans les Azure Functions vous épargne tous les problèmes qu’on pourrait rencontrer avec une utilisation normale du HttpClient. Bien s’en servir, et bien utiliser les lifetimes des services dans l’injection de dépendances va garantir un fonctionnement stable et de longue durée sur vos intégrations par Azure Functions.