Compatibilité

Cette page fournit des explications plus détaillées sur la liste des modifications radicales et non radicales présentées dans la section Gestion des versions.

Il n'est pas toujours évident de savoir ce qu'est une modification radicale (incompatible). Les conseils donnés ici devraient être considérés comme des indications plutôt que comme une liste complète de toutes les modifications possibles.

Les règles répertoriées ne concernent que la compatibilité avec le client. Les producteurs d'API devraient connaître leurs propres exigences en matière de déploiement, y compris de modifications apportées aux détails de la mise en œuvre.

L'objectif général est que les clients ne soient pas interrompus par une mise à jour de service vers une nouvelle version mineure ou un correctif. Les types de faille à l'étude sont :

  • La compatibilité avec la source : le code écrit avec la version 1.0 n'a pas été compilé avec la version 1.1.
  • La compatibilité avec le fichier binaire : le code compilé avec la version 1.0 n'a pas pu être lié/exécuté sur une bibliothèque cliente 1.1. (Les détails exacts dépendent de la plate-forme du client. Il existe des variantes dans différentes situations.)
  • La compatibilité en communication : une application créée avec la version 1.0 n'a pas réussi à communiquer avec un serveur 1.1.
  • La compatibilité sémantique : tout fonctionne mais donne des résultats inattendus ou surprenants.

En d'autres termes, les anciens clients devraient pouvoir utiliser des serveurs plus récents avec le même numéro de version majeure et, lorsqu'ils souhaitent passer à une nouvelle version mineure (par exemple, pour profiter d'une nouvelle fonctionnalité), ils devraient pouvoir le faire sans difficultés.

Remarque : Lorsque nous faisons référence aux numéros de version, tels que la version 1.1 et la version 1.0, nous faisons référence aux numéros de version logiques qui ne sont jamais rendus concrets. Leur but est simplement de faciliter la description des modifications.

En plus des considérations théoriques reposant sur des protocoles, il existe des considérations pratiques dues à l'existence de bibliothèques clientes impliquant à la fois du code généré et du code manuscrit. Dans la mesure du possible, testez les modifications que vous envisagez en générant de nouvelles versions des bibliothèques clientes et en vous assurant que leurs tests sont toujours concluants.

La discussion ci-dessous divise les messages proto en trois catégories :

  • Messages de requête (tels que GetBookRequest)
  • Messages de réponse (tels que ListBooksResponse)
  • Messages de ressource (tels que Book, y compris les messages utilisés dans d'autres messages de ressources)

Ces catégories observent des règles différentes. Les messages de requête ne sont envoyés que du client au serveur, les messages de réponse ne sont envoyés que du serveur au client, mais les messages de ressource sont généralement envoyés dans les deux sens. En particulier, les ressources pouvant être mises à jour doivent être considérées en termes de cycle lecture/modification/écriture.

Modifications rétrocompatibles (non radicales)

Ajouter une interface API à une définition de service d'API

D'un point de vue protocolaire, cette méthode est toujours sûre. La seule mise en garde concerne le fait que les bibliothèques clientes ont peut-être déjà utilisé votre nouveau nom d'interface API dans un code manuscrit. Si votre nouvelle interface est entièrement indépendante par rapport aux interfaces existantes, cela est peu probable. S'il s'agit d'une version simplifiée d'une interface existante, cela risque davantage de provoquer un conflit.

Ajouter une méthode à une interface API

À moins d'ajouter une méthode qui entre en conflit avec une méthode déjà générée dans les bibliothèques clientes, cela devrait suffire.

(Exemple pouvant avoir un effet destructif : si vous utilisez une méthode GetFoo, le générateur de code C# aura déjà créé les méthodes GetFoo et GetFooAsync. L'ajout d'une méthode GetFooAsync dans votre interface d'API constituerait donc une modification destructive du point de vue de la bibliothèque cliente.)

Ajouter une liaison HTTP à une méthode

En supposant que la liaison n'introduit pas d'ambiguïté, il est considéré comme sûr que le serveur réponde à une URL qu'il aurait précédemment rejetée. Cette action peut être exécutée lorsqu'une opération existante est appliquée à un nouveau format de nom de ressource.

Ajouter un champ à un message de requête

L'ajout de champs de requête peut se faire sans interruption, à condition que les clients qui ne spécifient pas le champ soient traités de la même manière dans la nouvelle version et dans l'ancienne version.

L'exemple le plus représentatif de la manière incorrecte d'appliquer cette modification concerne la pagination. Si la version 1.0 de l'API n'inclut pas la pagination pour une collection, elle ne peut pas être ajoutée dans la version 1.1, sauf si l'attribut page_size par défaut est traité de façon infinie (ce qui n'est généralement pas une bonne idée). Sinon, les clients v1.0 qui s'attendent à obtenir des résultats complets à partir d'une seule requête risquent de recevoir des résultats tronqués, sans avoir conscience que la collection contient plus de ressources.

Ajouter un champ à un message de réponse

Un message de réponse qui n'est pas une ressource (par exemple, ListBooksResponse) peut être développé sans effet destructif pour les clients, tant que cela ne modifie pas le comportement des autres champs de réponse. Tout champ précédemment renseigné dans une réponse doit continuer à être rempli avec la même sémantique, même si cela introduit une redondance.

Par exemple, une réponse à une requête dans la version 1.0 peut inclure un champ booléen contained_duplicates pour indiquer que certains résultats ont été omis en raison de la duplication. Dans la version 1.1, il serait possible de fournir des informations plus détaillées dans un champ duplicate_count. Même si cela est redondant dans la version 1.1, le champ contained_duplicates doit tout de même être renseigné.

Ajouter une valeur à une énumération

Une énumération qui est uniquement utilisée dans un message de requête peut être librement étendue pour inclure de nouveaux éléments. Par exemple, le modèle Vue des ressources permet d'ajouter une vue dans une nouvelle version mineure. Les clients n’ont jamais besoin de recevoir cette énumération, ils ne doivent donc pas être au courant des valeurs qui ne les intéressent pas.

Pour les messages de ressource et les messages de réponse, l'hypothèse par défaut est que les clients doivent gérer des valeurs d'énumération qu'ils ignorent. Toutefois, les créateurs d’API doivent savoir qu’écrire des applications pour gérer correctement les nouveaux éléments d'énumération peut s’avérer difficile. Les propriétaires d'API devraient documenter le comportement attendu du client lorsqu'ils rencontrent une valeur d'énumération inconnue.

Proto3 permet aux clients de recevoir une valeur qu'ils ignorent et de sérialiser à nouveau le message en conservant la même valeur. Cela permet au cycle lecture/modification/écriture de ne pas être interrompu. Le format JSON permet d'envoyer une valeur numérique dont le "nom" est inconnu, mais le serveur ne sait généralement pas si le client a connaissance ou non d'une valeur particulière. Les clients JSON peuvent donc savoir qu'ils ont reçu une valeur qui leur était inconnue auparavant, mais ils ne verront que le nom ou le numéro, ils ne connaîtront pas les deux. Le renvoi de la même valeur au serveur dans un cycle lecture/modification/écriture ne doit pas modifier ce champ, car le serveur doit comprendre les deux formulaires.

Ajouter un champ de ressource en sortie seulement

Les champs d'une entité de ressource provenant exclusivement du serveur peuvent être ajoutés. Le serveur peut valider que toute valeur fournie par le client dans une requête est valide, mais elle ne doit pas échouer si la valeur est omise.

Effectuer des modifications (radicales) incompatibles avec les versions antérieures

Supprimer ou renommer une valeur de service, de champ, de méthode ou d'énumération

Fondamentalement, si le code client peut faire référence à quelque chose, le supprimer ou le renommer représente une modification radicale qui doit entraîner une augmentation majeure de la version. Le code faisant référence à l'ancien nom provoquera des échecs lors de la compilation dans certains langages (tels que C# et Java). Il peut également provoquer des échecs au moment de l'exécution ou une perte de données dans d'autres langages. La compatibilité avec le format de communication n'a pas son importance ici.

Modifier une liaison HTTP

Dans ce contexte, "modifier" signifie "supprimer et ajouter". Par exemple, si vous décidez que vous souhaitez réellement être compatible avec PATCH mais que votre version publiée accepte la méthode PUT ou que vous avez utilisé un nom de verbe personnalisé incorrect, vous pouvez ajouter la nouvelle liaison, mais vous ne devez pas supprimer l'ancienne liaison pour ces mêmes raisons, car la suppression d’une méthode de service est une modification radicale.

Modifier le type d'un champ

Même lorsque le nouveau type est compatible avec le format de communication, le code généré est susceptible d'être modifié pour les bibliothèques clientes et doit donc entraîner une augmentation majeure de la version. Pour les langages compilés de type statique, cela peut facilement introduire des erreurs lors de la compilation.

Modifier un format de nom de ressource

Une ressource ne doit pas changer de nom, ce qui signifie que les noms de collection ne peuvent pas être modifiés.

Contrairement à la plupart des modifications radicales, cela concerne également les versions majeures : si un client peut s'attendre à utiliser l'accès v2.0 à une ressource créée dans la v1.0 ou inversement, le même nom de ressource doit être utilisé dans les deux versions.

Plus subtilement encore, l'ensemble des noms de ressources valides ne devrait pas non plus changer, pour les raisons suivantes :

  • En cas de restriction accrue, une requête qui aurait précédemment abouti échouerait désormais.
  • En cas de restriction réduite par rapport à ce qui a été documenté auparavant, les clients qui émettent des hypothèses reposant sur la documentation précédente pourraient s'avérer défectueux. Il est très probable que les clients stockent les noms de ressources ailleurs, d'une manière qui soit sensible à l'ensemble de caractères autorisés et à la longueur du nom. Sinon les clients peuvent également effectuer leur propre validation de nom de ressource pour se conformer à la documentation. (Par exemple, Amazon a averti ses clients et leur a accordé une période de migration pour autoriser des identifiants de ressources EC2 plus longs.)

Notez qu'une telle modification est visible uniquement dans la documentation d'un proto. Par conséquent, lorsque vous examinez une ligne de commande pour détecter une faille, il ne suffit pas d'examiner les modifications sans commentaire.

Modifier le comportement visible des requêtes existantes

Les clients dépendent souvent de la sémantique et du comportement de l'API, même si un tel comportement n'est pas explicitement accepté ou documenté. Ainsi, dans la plupart des cas, le changement de comportement ou de sémantique des données de l'API sera perçu comme un changement radical par les consommateurs. Si le comportement n'est pas cryptographiquement masqué, vous devez supposer que les utilisateurs l'ont découvert et dépendent de lui.

Il est également conseillé de chiffrer les jetons de pagination pour cette raison (même lorsque les données ne sont pas intéressantes), afin d’empêcher les utilisateurs de créer leurs propres jetons et d’être potentiellement endommagés lorsque le comportement du jeton change.

Modifier le format de l'URL dans la définition HTTP

Il existe deux types de modification à prendre en compte ici, au-delà des modifications de nom de ressource répertoriées ci-dessus :

  • Noms de méthode personnalisés : bien qu'ils ne fassent pas partie du nom de la ressource, un nom de méthode personnalisé fait partie de l'URL publiée par les clients REST. La modification d'un nom de méthode personnalisé ne devrait pas interrompre les clients gRPC, mais les API publiques doivent supposer la présence de clients REST.
  • Noms de paramètres de ressource : le passage de v1/shelves/{shelf}/books/{book} à v1/shelves/{shelf_id}/books/{book_id} n'affecte pas le nom de la ressource substitué, mais peut affecter la génération de code.

Ajouter un champ en lecture/écriture à un message de ressource

Les clients effectuent souvent des opérations de lecture/modification/écriture. La plupart des clients ne fournissent pas de valeurs pour les champs dont ils n'ont pas connaissance, et proto3 en particulier n'est pas compatible avec ce principe. Vous pouvez spécifier que les champs manquants des types de message (plutôt que les types primitifs) signifient qu'aucune mise à jour n'est appliquée à ces champs. Toutefois, cela rend plus difficile la suppression explicite d'une telle valeur de champ au sein d'une entité. Les types primitifs (y compris string et bytes) ne peuvent tout simplement pas être traités de cette façon, car il n'y a aucune différence dans proto3 entre définir explicitement un champ int32 sur 0 et ne pas le spécifier du tout.

Si toutes les mises à jour sont effectuées à l'aide d'un masque de champ, le problème ne se pose pas, car le client n'écrase pas implicitement les champs dont il n'a pas connaissance. Cependant, cette décision serait inhabituelle pour les API, car la plupart des API autorisent les mises à jour de "ressources complètes".