Changements d'API

Changements cassants Protobuf : ce qui survit sur le fil

7 min de lecture

Une API REST change quand une forme JSON change, et la majeure partie de cette forme est visible dans la réponse qu’on peut lire dans un navigateur. Une API gRPC change quand un fichier .proto change, et le format binaire sur le fil de Protocol Buffers a ses propres règles sur ce qu’un client peut tolérer, qui n’ont rien à voir avec ce que disent les noms de champs. Deux modifications qui paraissent également petites dans un diff, renuméroter un champ contre en ajouter un, tombent des côtés opposés d’une ligne que changements cassants trace en général : l’une est invisible pour chaque client existant, l’autre les casse tous d’un coup. Distinguer les changements cassants Protobuf des changements sûrs veut dire lire les règles propres du format sur le fil, pas deviner d’après l’apparence du changement dans un diff .proto.

Pourquoi la numérotation des champs compte-t-elle plus que le nom du champ dans Protobuf ?

Parce que le format sur le fil encode les champs par numéro, pas par nom. Le code généré dans chaque langage lit et écrit ces numéros ; le nom de champ email dans votre fichier .proto est une commodité pour les humains qui ne touche jamais les octets binaires envoyés sur le réseau. Renommer un champ, email en email_address, est sûr sur le fil binaire tant que le numéro reste le même, ce qui surprend les ingénieures habituées à REST, où une clé JSON renommée est exactement le type de changement qui casse un client. L’exception est ce même cas REST : les formats ProtoJSON et texte sérialisent le nom, donc un renommage casse le transcodage JSON (un grpc-gateway, par exemple), les fichiers au format texte et les field masks. Renuméroter ce même champ, en gardant le nom mais en changeant 1 en 7, est exactement l’inverse : invisible dans une revue de code qui ne montre que les noms, et ça corrompt chaque message qu’un client envoie ou reçoit à partir de ce moment-là.

ChangementSûr sur le filPourquoi
Renommer un champ, garder son numéroBinaire oui, JSON et texte nonL’encodage binaire utilise le numéro ; ProtoJSON et le format texte utilisent le nom
Changer le numéro d’un champNonChaque message existant est maintenant lu comme le mauvais champ
Ajouter un nouveau champ avec un nouveau numéroOuiLes anciens clients ignorent les champs qu’ils ne reconnaissent pas
Supprimer un champ, réutiliser son ancien numéro pour autre choseNonLes anciennes données se décodent dans le mauvais nouveau champ
Changer le type d’un champ de façon incompatible (ex. int32 en string)NonL’encodage sur le fil diffère selon le type

Qu’est-ce qui rend la suppression d’un champ différente de celle dans une réponse JSON REST ?

Le numéro devient radioactif. Les directives de Protobuf elles-mêmes recommandent de marquer le numéro d’un champ supprimé comme reserved plutôt que de le laisser être réutilisé, parce que la réutilisation est où le vrai dégât se produit : un client qui fait encore tourner du code généré du mois dernier envoie un message utilisant l’ancien numéro du champ pour l’ancienne signification, et le serveur, qui s’attend maintenant à ce que ce numéro signifie autre chose, mal-interprète les données en silence au lieu de les rejeter carrément. REST n’a pas de piège équivalent, parce qu’une clé JSON supprimée arrête simplement d’apparaître ; il n’y a aucun moyen pour la requête d’un ancien client d’être silencieusement réinterprétée comme autre chose. Un fichier .proto avec reserved 4, 9, 12; en haut d’un message est une cicatrice permanente, et c’est exactement le but : ça empêche le numéro d’être attribué à un nouveau champ par quelqu’un qui n’en connaissait pas l’histoire.

message Invoice {
  reserved 4; // était `legacy_customer_id`, supprimé le 2026-06-01
  reserved "legacy_customer_id"; // le nom aussi, pour JSON/texte
  string customer_id = 5;
  string status = 6;
}

Ajouter un champ nécessite-t-il jamais une entrée de changelog ?

Généralement pas une entrée de changement cassant, mais souvent une entrée normale, parce que « sûr sur le fil » et « invisible pour une lectrice à qui ça importe » sont deux affirmations différentes. Ajouter un champ à un message de réponse ne coûte rien structurellement, les anciens clients décodent le message et ignorent le nouveau champ automatiquement. Mais quelqu’un qui construit une nouvelle intégration contre ce service n’a aucun moyen de savoir que le champ existe à moins que quelqu’un le lui dise, parce que rien dans un build réussi ou un test qui passe ne rend visible un nouveau champ optionnel. Changelog d’API couvre en général ce qu’une entrée additive doit à quelqu’un qui la lit ; la raison spécifique à gRPC d’en écrire une quand même, c’est qu’il n’y a pas d’équivalent à parcourir une réponse REST dans un débogueur pour remarquer qu’une nouvelle clé est apparue.

En quoi est-ce différent de ce à quoi font face les appelants GraphQL ?

Les règles pour les ajouts sont les mêmes, mais l’exposition diffère. Dépréciation de schéma GraphQL couvre un modèle où un client ne reçoit que les champs qu’il demande explicitement, ce qui rend les changements additifs essentiellement sans risque et les suppressions le seul vrai danger. Les clients gRPC, à l’inverse, reçoivent tout ce que le serveur envoie et décodent tout contre leur propre copie compilée du schéma ; l’exposition d’un client n’est pas limitée par ce qu’il a demandé, seulement par ce que son code généré sait lire. Cette différence compte pour écrire des changelogs : une entrée GraphQL peut raisonnablement supposer que les clients sont protégés des champs qu’ils n’ont pas demandés, et une entrée gRPC ne peut pas du tout supposer ça.

Versionner un service gRPC fonctionne-t-il comme les /v1/, /v2/ de REST ?

Le mécanisme est différent même quand l’intention est la même. Que sont v1 et v2 dans une API REST couvre le versionnage comme des chemins d’URL parallèles servant des contrats différents ; les services gRPC versionnent typiquement via le nom de package dans le fichier .proto lui-même, payments.v1.InvoiceService devenant payments.v2.InvoiceService, ce qui change le nom de service complètement qualifié qu’un client compose plutôt qu’un segment d’URL qu’il demande. Les deux approches résolvent le même problème, laisser un ancien contrat continuer de fonctionner pendant qu’un nouveau existe, mais une équipe venant d’un contexte REST cherche souvent un numéro de version au mauvais endroit et rate que la déclaration de package fait ce travail.

Que devrait vraiment nommer une entrée de changelog gRPC ?

Le message, le numéro de champ, et si c’est additif ou une suppression nécessitant une migration, dans cet ordre d’importance pour une lectrice décidant d’agir ou non. « Ajout de shipping_address (champ 8) à Order » dit à une intégratrice tout ce dont elle a besoin pour mettre à jour le code généré et commencer à l’utiliser. « Champ 4 réservé sur Invoice, legacy_customer_id a disparu » lui dit de vérifier si quelque chose dans son code lit encore ce champ, ce qu’une note style REST « champ supprimé de la réponse » ne communique pas avec la même urgence, parce que les suppressions REST retournent simplement moins de données alors que la réutilisation de champ Protobuf les corrompt activement.

FAQ

Le type d’un champ peut-il jamais être changé sans casser le format sur le fil ? Seulement dans des groupes compatibles spécifiques que Protobuf documente, comme élargir int32 en int64 dans certains cas. Traitez tout changement de type comme cassant à moins de l’avoir vérifié contre la table de compatibilité de Protobuf elle-même ; supposer une compatibilité par analogie avec le système de types d’un langage est comment ça tourne mal.

Déprécier un champ dans Protobuf fonctionne-t-il comme la directive @deprecated de GraphQL ? De façon similaire : Protobuf supporte une option de champ [deprecated = true] que l’outillage peut afficher. Aucune des deux n’est appliquée : un serveur GraphQL répond toujours à une requête sur un champ déprécié, et un client protobuf en encode toujours un. Les deux sont indicatives et nécessitent le même appui de changelog.

Renuméroter est-il jamais sûr si vous contrôlez chaque client ? Dans un système entièrement fermé, en principe, mais ça élimine toute la propriété de sécurité pour laquelle les numéros de champ existent, et « on contrôle chaque client » est une affirmation qui cesse d’être vraie dès qu’un build est mis en cache, qu’un déploiement est retardé, ou qu’un client est ajouté dont personne ne se souvenait. Réservez le numéro plutôt que de le réutiliser, même en interne.

Les services gRPC ont-ils besoin d’une page de changelog comme une API REST publique ? Seulement si des équipes externes les consomment sans lire directement les diffs .proto, le même test « qui est de l’autre côté » que les changelogs d’API interne applique en général. Un service gRPC consommé seulement par d’autres services de la même équipe peut souvent se passer d’un changelog formel en faveur de l’historique des commits, parce que quiconque le lit a déjà le schéma ouvert.


Les affirmations techniques de cet article n'ont pas été vérifiées de façon indépendante. Si quelque chose est faux, dis-le-nous et nous le corrigerons.

À voir sur changeloop : Documentation développeurs, Comparatif d'outils de changelog

changeloop
L'équipe qui construit un changelog qui boucle la boucle. Tes utilisateurs demandent, ton équipe livre, la personne qui a demandé est prévenue.