François Hyvrier
Jan 14 · 8 min read
Minitel 10, A1AA1A (CC BY-SA 4.0)

Cet article revient sur l’évolution de notre outillage pour les communications HTTP entre les différentes applications qui composent notre système d’informations.

Le framework .NET sur lequel repose la plupart de nos applications propose la classe HttpClient qui permet d’implémenter simplement l’envoi de requêtes HTTP et la réception de réponses HTTP. Cette classe offre aussi aux développeurs la possibilité de personnaliser assez finement les requêtes envoyées (comme le format de leur contenu, leurs headers HTTP, la durée de timeout, etc.).

Lorsque notre système d’informations initialement monolithique a commencé à évoluer vers une architecture distribuée inspirée des micro-services, une partie de ses fonctionnalités ont été déplacées vers de nouvelles applications avec lesquelles il fallait désormais communiquer via des requêtes HTTP de façon sécurisée.


Besoin de corréler des logs

Toutes les applications enregistrent des logs de leur exécution, ce qui permet notamment d’avoir les détails des erreurs qui peuvent survenir. Dans un système distribué, un traitement peut être exécuté dans sa globalité par plusieurs applications et il est vite difficile de relier les traces de son exécution complète à travers toutes les applications impliquées.

Une solution à ce problème consiste à générer un identifiant de corrélation unique pour chaque traitement à effectuer et le transmettre à toutes les applications prenant part à son exécution. Cet identifiant sera ensuite inclus systématiquement dans leurs logs.

Exemple d’implémentation de la corrélation

Du point de vue d’une application devant en appeler une autre, au moment d’envoyer la requête HTTP, on ajoute l’identifiant de corrélation du traitement ainsi que le nom de l’application courante.

Du côté de l’application appelée, il suffit de récupérer l’identifiant de corrélation et le nom de l’appelant et de les inclure dans tous les logs suivants. On est de cette façon capable de relier les traces de l’appelant et celles de l’appelé et d’avoir une vision complète de ce qui s’est passé pour ce traitement en particulier.


Besoin de d’appeler les applications de manière sécurisée

Nos applications web sont sécurisées avec Azure Active Directory : pour pouvoir les appeler, il est nécessaire :

  1. De s’authentifier auprès d’une autorité, qui en cas de succès retourne un jeton (un token JWT) : il s’agit d’une chaîne de caractères.
  2. Le token retourné par l’autorité doit ensuite être transmis à l’application appelée via le header HTTP Authorization de la requête.

A la réception de la requête, l’application récupère le token dans le header, vérifie qu’il est valide et si c’est bien le cas, effectue le traitement.

Exemple d’implémentation d’un appel authentifié grâce à un certificat

Première version de notre client HTTP

Afin d’éviter de dupliquer le code permettant d’ajouter l’identifiant de corrélation et celui responsable d’obtenir un token Azure Active Directory partout où une requête HTTP doit être envoyée, une première classe utilitaire a été créée pour factoriser ces comportements.

Cette classe ressemble à peu près à cela :

Cette classe abstraite encapsule une instance de HttpClient, elle expose des méthodes permettant d’envoyer des requêtes HTTP de tout type (GET, POST, PUT, etc.) et se charge d’ajouter automatiquement les headers nécessaires à la corrélation et l’authentification aux requêtes HTTP envoyées.

A la réception de la réponse, elle vérifie si le statut indique une erreur et si c’est le cas, une exception est levée.

La classe s’utilise de la façon suivante :

La classe CustomerApiClient qui implémente l’appel à une web API accessible à l’URL https://customer-api.com hérite de la classe abstraite WebApiClient et utilise sa méthode GetAsync<T>() pour envoyer une requête HTTP incluant des informations de corrélation ainsi qu’un token Azure Active Directory.

Grâce à cette classe, l’implémentation d’un client d’une de nos web API est plus rapide : le développeur ne se concentre que sur les URL à appeler et les types d’objets à envoyer à l’API. Il n’est plus nécessaire de se préoccuper de la logique pour s’authentifier ou pour inclure des identifiants de corrélation car c’est la classe WebApiClient qui s’en charge.

Cette première version présente toutefois quelques problèmes :

  • Elle n’est pas thread safe à cause des appels à _httpClient.DefaultRequestHeaders.Add() qui ont un effet de bord sur l’instance de HttpClient.
  • Elle ne permet pas de gérer finement les différents statuts HTTP que peuvent retourner l’API car un statut autre que 2xx entraîne une exception. Ce comportement ne posait pas de problème initialement, car à l’époque nos applications exploitaient peu les différents statuts d’erreur : une erreur de type 4xx (la requête envoyée par l’appelant est incorrecte), ou une erreur de type 5xx (l’application appelée a eu un problème) étaient traitées de la même façon.
  • Les classes qui en héritent ne sont pas testables facilement. En effet il n’est pas possible de mocker les appels HTTP de la classe WebApiClient.

Une nouvelle version plus adaptée à nos API RESTful

Nos applications web évoluent, elles utilisent notamment de plus en plus toute la gamme des statuts HTTP pour donner plus de sens aux réponses qu’elles retournent, comme les codes d’erreurs (entre 400 et 599) pour préciser les problèmes qui pourraient survenir.

Les clients de nos web API quant à eux gèrent de plus en plus finement ces statuts et implémentent des comportements différents selon le statut. Par exemple, une application qui recevrait une erreur de type 500 (Internal server error) pourrait attendre quelques secondes et répéter sa requête dans l’espoir que le problème interne du serveur ne soit que temporaire et que la deuxième requête réussisse. Si le problème persiste, le traitement peut être mis de côté et ressayé ultérieurement. Dans le cas d’une erreur de type 400 (Bad request) cette fois-ci, la même application ne réessayera pas de renvoyer sa requête car elle est manifestement erronée, elle pourra par exemple lever une exception pour signaler qu’il y a un bug.

La première version de notre client HTTP ne permettait pas d’implémenter ces comportements, nous avons donc réfléchi à une nouvelle classe prenant en compte ces besoins. L’objectif était de continuer de proposer un outil permettant aux développeurs d’implémenter la corrélation des appels HTTP ainsi que l’authentification avec Azure Active Directory, mais en leur laissant la responsabilité de gérer le statut HTTP comme ils le veulent et en rendant possible les tests unitaires.

Le bout de code suivant montre à quoi ressemble cette solution :

Voici les différences principales de cette classe avec la précédente :

  • Il ne s’agit plus d’une classe abstraite mais d’une classe concrète, cela évite de se reposer sur de l’héritage pour écrire des clients d’API, on peut à présent plus facilement utiliser la composition. Le fait qu’elle implémente une interface (IRestClient) lui permet aussi d’être mockée, ce qui facilite l’écriture de tests unitaires.
  • Les requêtes sont créées à l’aide du constructeur HttpRequestMessage, et les headers HTTP sont ajoutés à la requête et plus à l’instance de HttpClient servant à envoyer la requête. Ce changement fait que la classe RestClient est thread safe, contrairement à WebApiClient.
  • Une fois la réponse HTTP reçue, elle est retournée sous la forme d’une instance de la classe RestResponse<T>, qui est un wrapper de la classe HttpResponseMessage et expose des méthodes permettant d’accéder au statut de la réponse, à son contenu. Contrairement à WebApiClient, la classe RestClient n’est plus responsable de lever automatiquement une exception lorsque le statut de la réponse HTTP indique une erreur. C’est à présent au développeur de gérer ces codes lui-même et d’implémenter éventuellement des traitements différents selon les codes.

Voici un exemple d’utilisation de la nouvelle solution :

Cette nouvelle classe répondait aux problèmes que nous avions et son utilisation s’est généralisée dans les différentes applications de notre système d’informations. Nous avons tout de même commencé à réaliser que les classes que nous avions écrites faisaient un peu trop de choses par rapport au besoin initial : alors que nous voulions simplement fournir quelques fonctionnalités lors de l’envoi de requêtes HTTP, nous avions créé pour les implémenter des classes qui sont des wrappers autour des classes HttpClient et HttpResponseMesage du framework .NET, et qui masquent leurs fonctionnalités plus avancées dont nous avons parfois besoin.


Une solution plus ciblée avec les handlers de messages

La solution à chacun des besoins initiaux (corrélation et authentification) a consisté à implémenter le comportement suivant.

Pour chaque requête HTTP à envoyer :

  1. Obtenir une chaîne de caractères (un id de corrélation, le nom d’une application ou un token JWT certifiant qu’un utilisateur est authentifié).
  2. Ajouter la chaîne de caractères obtenue à la requête HTTP sous la forme d’un header.
  3. Envoyer la requête HTTP grâce à la classe HttpClient.

Il se trouve qu’il existe une classe dans le framework .NET qui permet justement de venir insérer ce genre de logique dans le pipeline de traitement d’une requête HTTP: DelegatingHandler.

En créant une classe qui hérite de DelegatingHandler et en en fournissant une instance au HttpClient qui servira à envoyer les requêtes HTTP, on peut agir comme on le veut sur le processus d’envoi de requête et de lecture de la réponse : ajouter des headers, logger les requêtes, les réponses, les temps d’exécution, implémenter des mécanismes de retry, voire même mocker les réponses HTTP.

Exemple : un handler qui ajoute un header à toutes les requêtes HTTP à envoyer et qui logge chaque réponse HTTP reçue.

Le handler s’utilise ensuite lors de la création du HttpClient :

On peut aussi configurer son HttpClient pour utiliser plusieurs handlers : ils seront appelés en cascade à chaque envoi de requête HTTP. Cela permet de composer son HttpClient pour lui injecter toutes les fonctionnalités que l’on souhaite, chacune implémentée par un handler dédié.

Dans l’exemple précédent, chaque requête HTTP envoyée sera interceptée par trois handlers, dans cet ordre :

  1. AccessTokenHandler : obtiendra un token Azure Active Directory et l’ajoutera en tant que header « Authorization » à la requête à envoyer.
  2. CorrelationIdHandler : ajoutera un identifiant de corrélation en tant que header « X-CorrelationId » à la requête.
  3. CallerNameHandler : ajoutera le nom de l’application courante en tant que header « X-CallerApplicationName » à la requête.

Au lieu de distribuer aux développeurs des wrappers des classes du framework que sont HttpClient et HttpResponseMessage, nous privilégions maintenant la mise à disposition de handlers de messages qui répondent de façon beaucoup plus ciblée à nos besoins de corrélation et d’authentification.


Conclusion

Notre besoin d’outils HTTP est né du découpage de notre système d’informations en différentes applications. Les communications étaient alors essentiellement intraprocessus (tous les appels de méthodes sont réalisés au sein d’un seul processus où s’exécute notre application monolithique), et il a fallu se mettre à appeler d’autres applications (communication interprocessus) via le protocole HTTP.

Nos outils ont ensuite été adaptés pour accompagner l’évolution de nos applications et de nos connaissances de l’environnement .NET : des premières versions qui avaient un peu trop de responsabilités, n’étaient pas testables facilement et masquaient des fonctionnalités avancées, nous sommes passés à des composants plus légers, plus spécialisés, moins intrusifs dans le code des développeurs et exploitant mieux les mécanismes d’extensibilité du framework .NET.

Quelques liens sur le sujet :

YounitedTech

Le blog Tech de Younited, où l’on parle de développement, d’architecture, de microservices, de cloud, de data… Et de comment on s’organise pour faire tout ça. Ah, et on recrute aussi, on vous a dit ?

Thanks to Nicholas Suter

François Hyvrier

Written by

YounitedTech

Le blog Tech de Younited, où l’on parle de développement, d’architecture, de microservices, de cloud, de data… Et de comment on s’organise pour faire tout ça. Ah, et on recrute aussi, on vous a dit ?

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade