Les changements incompatibles d’API, expliqués

Un changement incompatible d’API est tout changement apporté au contrat publié d’une API qui peut faire échouer, contre le nouveau contrat, un client qui avait été écrit correctement contre l’ancien — sans que le client ne change rien. Cette phrase unique est toute la théorie ; tout le reste de cet article en sont les conséquences : quels changements comptent, quels pièges célèbres semblent inoffensifs et ne le sont pas, comment examiner une montée de spécification avant qu’elle ne se déploie, et comment les stratégies de versionnage achètent du temps aux clients. Pour l’appliquer à deux documents réels, lancez OpenAPI Diff sur l’ancienne et la nouvelle spécification et lisez les verdicts à côté de cette page.

Un contrat est une promesse sur l’avenir

Une spécification d’API n’est pas de la documentation au sens désinvolte ; c’est une promesse. Quand une spécification déclare que GET /pets accepte un paramètre facultatif limit et répond 200 avec un objet contenant une chaîne name, chaque auteur de client construit exactement là-dessus : le paramètre est omis quand il n’est pas nécessaire, le statut sert de branche, le champ est lu comme une chaîne. La correction du client se juge contre la promesse, et c’est pourquoi la définition d’incompatible est asymétrique — un changement est incompatible si un ancien client correct peut échouer, même si la plupart des clients survivent par hasard. Un SDK strict généré depuis l’ancienne spécification, échouant sur un type changé, suffit : c’est la promesse qui a été rompue, pas le client.

Ce cadre résout la plupart des débats sur le fait qu’un changement « compte ». Supprimer un point de terminaison est incompatible parce que la promesse incluait son existence. En ajouter un ne l’est pas, parce qu’aucun ancien client ne s’est rien promis au sujet de chemins qu’il n’appelle jamais. Un nouveau champ de réponse facultatif est sûr parce que les anciens analyseurs ignorent les champs inconnus : dans tout format sensé, la sortie additive ne peut pas invalider un lecteur. Une nouvelle entrée obligatoire, en revanche, atteint chaque requête existante et invalide celles qui l’omettent. Le test est toujours le même : rejouez le trafic valide d’hier contre les règles de demain et demandez ce qui échoue désormais.

Deux corollaires comptent en pratique. D’abord, le contrat est ce qui est publié, pas ce que le serveur fait aujourd’hui par hasard : un champ non documenté que les clients ont découvert en reniflant le trafic n’est pas promis, et le retirer est grossier mais pas, strictement, incompatible — ce qui plaide pour ne documenter que ce que vous avez l’intention de garder. Ensuite, le contrat inclut toute la surface des normes sur lesquelles il repose : la sémantique des méthodes et des statuts HTTP fixée par la RFC 9110 fait partie de la promesse, donc redéfinir ce que signifie un 200 ou un 404 est un changement de contrat même si le fichier de spécification ne mentionne jamais la signification des statuts.

Les pièges classiques

La casse de production la plus courante est l’ajout d’un champ obligatoire. Une équipe ajoute un champ à un corps de requête, le marque obligatoire parce que la nouvelle règle métier en a besoin, et publie — après quoi chaque client existant, dont aucun n’envoie le champ, commence à recevoir des 400. Les versions sûres sont ennuyeuses : ajoutez le champ comme facultatif avec une valeur par défaut côté serveur, ou ne l’exigez que sur un nouveau point de terminaison. Le deuxième piège est le retrait de valeurs d’énumération : une valeur de statut, une catégorie, un ordre de tri est retiré parce que la nouvelle interface ne l’offre plus, tandis que les données clientes stockées, les applications mobiles sur d’anciennes versions et les intégrations de partenaires continuent de l’envoyer pendant des années. Les valeurs d’énumération ne coûtent presque rien à continuer d’accepter ; en retirer une devrait être traité comme la suppression d’un point de terminaison.

Les changements de type sont le troisième piège, et ils viennent déguisés en rétrécissement et en élargissement. Le rétrécissement — de chaîne à énumération, d’entier à un intervalle plus petit, d’objet libre à forme fixe — rejette des entrées autrefois valides et est incompatible à vue d’œil. L’élargissement est plus subtil : changer un champ de réponse de chaîne à chaîne-ou-nul, ou un identifiant entier en identifiant chaîne, semble généreux et casse chaque analyseur strict et chaque SDK généré construit pour l’ancien type. Côté réponse, un piège miroir existe : un champ toujours présent qui devient facultatif. Les clients le lisent inconditionnellement ; la première réponse qui l’omet est un pointeur nul dans le code de quelqu’un d’autre.

Le quatrième piège est sémantique, et aucun comparateur ne peut l’attraper : le champ garde son nom et son type et change de sens. Un prix qui passe en silence des centimes aux dollars, un horodatage qui passe de l’UTC à l’heure locale, une liste qui était triée et ne l’est plus — le diff de spécification est vide et chaque client a tort. Les casses sémantiques sont la raison pour laquelle le diff mécanique est un plancher, pas un plafond, pour la revue : il dégage les changements qui ne peuvent casser personne, pour que l’attention humaine se concentre sur les changements qui le pourraient simplement.

Examiner une montée de spécification d’un fournisseur avant de déployer

Les fournisseurs publient constamment de nouvelles versions de spécification, et « monter le SDK et redéployer » est la façon dont les casses silencieuses atteignent la production. Une revue disciplinée prend quinze minutes. D’abord, obtenez les deux documents — la spécification contre laquelle votre déploiement tourne réellement aujourd’hui, et la candidate — et comparez-les mécaniquement au lieu de lire un journal des changements, parce que les journaux des changements sont des documents marketing : ils listent les fonctionnalités, pas les suppressions. OpenAPI Diff produit cette liste localement, ce qui compte quand l’API est interne et que sa spécification ne devrait pas être collée dans l’outil hébergé d’un étranger.

Ensuite, parcourez la liste des incompatibles comme une liste de contrôle contre votre propre usage. La plupart des casses du fournisseur sont théoriques pour vous : un point de terminaison supprimé que vous n’appelez jamais, un champ obligatoire nouveau sur une ressource que vous ne créez jamais. Les entrées dangereuses sont celles qui croisent votre trafic — c’est pourquoi la revue a besoin de votre inventaire d’appels, pas seulement du diff. Troisièmement, vérifiez la surface non documentée : les statuts sur lesquels vous branchez, les formes d’erreur que vous analysez, les en-têtes que vous lisez. La même habitude qui protège les consommateurs de webhooks — épingler et tester le contrat de charge utile, comme dans les signatures de webhook, expliquées — s’applique à tout contrat que quelqu’un d’autre contrôle.

Enfin, déployez par étapes. Pointez un client de préproduction vers la nouvelle version, rejouez des requêtes de production enregistrées (une capture assainie, traitée comme le prescrit le guide HAR), et comparez les réponses avant n’importe quel utilisateur. Si le fournisseur offre une fenêtre d’obsolescence ou un épinglage de version, prenez-le : le changement incompatible le moins cher est celui que vous adoptez à votre rythme, une fois vos clients prêts, plutôt que le jour de publication du fournisseur.

Stratégies de versionnage : URL, en-tête, ou ne jamais casser

La réponse classique au changement incompatible est un identifiant de version, et le débat classique est où le mettre. Le versionnage par URL (/v1/, /v2/) est explicite, favorable au cache, trivialement routable et visible dans chaque ligne de journal ; son coût est qu’il versionne toute l’API d’un coup, de sorte qu’un seul point de terminaison incompatible traîne chaque client dans une migration. Le versionnage par en-tête — un en-tête de version ou un schéma de négociation de contenu daté — garde les URL propres et peut versionner des ressources individuelles, au prix de l’invisibilité : la version disparaît des journaux, des liens et des barres du navigateur, et le débogage devient de l’archéologie. Aucun des deux n’a tort ; l’industrie paie simplement le coût de l’URL plus volontiers parce que l’opérabilité bat l’élégance à 3 heures du matin.

La stratégie plus profonde est d’avoir rarement besoin de versions. L’évolution seulement additive — ne jamais supprimer, ne jamais exiger, ne jamais retyper ; n’ajouter que des choses facultatives — garde une seule version en vie indéfiniment, ce qui est la façon dont les API publiques les plus durables opèrent en fait. Elle exige de la discipline dès le départ : une tolérance généreuse en entrée, des identifiants typés comme chaînes dès le premier jour (pour que l’intervalle puisse grandir), des énumérations conçues pour être étendues, et un processus d’obsolescence qui avertit longtemps avant de retirer quoi que ce soit. Le versionnage sémantique, tel que codifié sur semver.org, fournit la couche de signalisation que la propre spécification de l’OpenAPI Initiative laisse délibérément aux éditeurs : une montée majeure annonce une casse, et les clients peuvent s’épingler contre elle.

Quelle que soit la stratégie choisie, elle ne fonctionne que comme un contrat social soutenu par de la mécanique : les versions doivent réellement tourner côte à côte pendant une fenêtre déclarée, les obsolescences doivent être annoncées là où les auteurs de clients les verront (journal des changements, en-têtes de réponse, avis direct aux gros consommateurs), et l’usage de l’ancienne version doit être mesuré pour que « plus personne ne l’utilise » soit un fait, pas un espoir. Une version publiée mais qui ne peut pas fonctionner en parallèle n’est pas une stratégie ; c’est un renommage de la casse.

Où le comparateur s’insère

Un comparateur mécanique gagne sa place à deux moments. Dans la revue, il convertit « quelque chose a-t-il cassé ? » d’un après-midi de lecture côte à côte en une liste que l’on peut cocher — et, tout aussi utile, il prouve un négatif : quand une version prétend être seulement additive, une liste d’incompatibles vide est une preuve, pas des impressions. Dans l’intégration continue, la même comparaison peut tourner sur chaque demande de tirage qui touche une spécification, faisant échouer la construction quand un changement incompatible arrive sans la montée de version ou l’approbation que la politique du projet exige. Le tableau de classification est la politique ; l’outil refuse simplement de la laisser être oubliée à l’heure de la livraison.

Ses limites définissent le reste du flux de travail. Le comparateur ne lit que le contrat déclaré : il ne peut pas voir le comportement, la performance, la sémantique ni les particularités non documentées dont dépendent les clients, et il résume les schémas pathologiquement profonds au lieu de les développer à l’infini. La porte complète est donc un pipeline, pas une page : diff mécanique pour le contrat, tests de contrat rejouant de vrais appels clients pour le comportement, déploiement par étapes pour la sémantique, et supervision pour le jour où les trois laisseront passer quelque chose. Utilisé ainsi, le diff est la couche la moins chère des quatre — quinze secondes qui suppriment la classe de panne la plus embarrassante, celle où la casse était écrite dans la spécification depuis le début et personne n’a comparé les documents.

C’est cette habitude que cet article défend vraiment. Les spécifications sont bon marché à garder et encore moins chères à comparer ; les incidents ne le sont pas. Gardez chaque version publiée de chaque contrat que vous possédez ou consommez, comparez chaque candidate à la version en production avant qu’elle ne parte, et traitez la liste des incompatibles comme le véritable ordre du jour de la version. Les équipes qui font cela cessent de débattre pour savoir si un changement « aurait dû » casser quelqu’un — elles ont la liste, datée et archivée, d’avant sa publication.

Sources et lectures complémentaires

Questions fréquentes

Ajouter un champ obligatoire est-il toujours incompatible ?

Oui, sur tout point de terminaison que les clients existants appellent déjà : les requêtes valides hier sont rejetées aujourd’hui, ce qui est la définition d’incompatible. Les échappatoires standard sont d’ajouter le champ comme facultatif avec une valeur par défaut côté serveur, de ne l’exiger que sur un nouveau point de terminaison ou une nouvelle version, ou d’annoncer une fenêtre d’obsolescence pendant laquelle le champ est accepté mais signalé avant de devenir obligatoire.

Versionnage par URL ou par en-tête — lequel est le meilleur ?

Pour la plupart des équipes, celui par URL : il est visible dans les journaux et les liens, facile à router et à mettre en cache, et honnête sur ce qu’il est. Celui par en-tête convient aux plateformes qui ont besoin d’une granularité par ressource et qui ont déjà une forte observabilité. Les deux battent la pire option, le changement incompatible sans version. La meilleure stratégie de toutes est l’évolution seulement additive, qui fait du versionnage un événement rare plutôt qu’un impôt de routine.

Un outil peut-il attraper chaque changement incompatible ?

Non. Un comparateur attrape tout ce que la spécification déclare : suppressions, entrées obligatoires nouvelles, changements de type, retraits d’énumérations. Il ne peut pas attraper les changements sémantiques — même champ, nouveau sens — ni le comportement que la spécification n’a jamais déclaré. C’est pourquoi le diff mécanique appartient à un pipeline avec des tests de contrat et un déploiement par étapes, et non à un piédestal à lui.