Versioning de services REST

Introduction

Le versioning de service est un thème à part entière dans les architectures orientée service tant il structure l’évolutivité d’un service. Les architectures REST n’échappent pas à la règle. Posant les principes généraux de l’architecture, Roy Fielding n’en a pas pour autant décrit tous les méandres. Il n’appartient donc qu’à nous, développeurs et architectes, de définir ce qu’est le versioning des services REST en respectant ces principes. Ce billet traitera donc la problématique de versioning des services REST.

Quand versionne-t-on ?

Les types de changement

Il existe différents types de changements

  • On peut être amené à modifier des règles métiers soit parce qu’elles évoluent, soit parce qu’elles sont « bugfixées ».
  • On peut être amené à enrichir la signature du service pour répondre à de nouveaux besoins.

Souvent le premier cas n’a pas d’impact sur la signature du service et nous le laisserons volontairement en dehors du scope de cet article même si quelques problématiques de versioning se posent.

Dans le second cas, les évolutions de signature (c’est-à-dire principalement des paramètres d’appels et de réponse) peuvent être soit compatible avec les versions antérieures soit incompatibles.

Changement compatible

Ce sont les changements qui respectent le principe de compatibilité ascendante. Ces changements n’ont aucun impact sur les clients existants, pour peu qu’ils soient correctement implémentés.

C’est-à-dire qu’il respecte les pratiques suivantes :

  • Le client ne doit pas tenir compte de l’ordre des éléments dans une représentation (XML par exemple)
  • Le client ne doit pas échouer lorsque des éléments sont rajoutés dans une représentation (XML par exemple)
  • Le client ne doit pas échouer lorsque de nouveaux codes retours sont ajoutés
  • Le client d’une représentation ne doit pas échouer lorsqu’une nouvelle représentation est ajoutée

Par exemple, pour une ressource Album représentée en XML :

1
2
3
<album>
<title>Dark Side of the Moon</title>
</album>

L’ajout d’un champ ne modifie en rien la structuration précédente du message.

1
2
3
4
<album>
<title>Dark Side of the Moon</title>
<artist>Roger Waters</artist>
</album>

Les clients existants ne doivent pas être impactés.

Les changements incompatibles

Les changements dit incompatibles impactent directement les clients existants. Sur l’exemple précédent, si un ou plusieurs clients utilisent l’élément « artist », le fait de passer d’un artiste par album à une liste d’artistes par album rend cet élément inutilisable par ces clients.

1
2
3
4
5
6
7
<album>
<title>Dark Side of the Moon</title>
<artists>
<artist>Roger Waters</artist>
<artist>David Gilmour </artist>
</artists>
</album>

Un client, même codé selon les règles de l’art du parsing XML, ne pourra pas absorber la modification. (nom, granularité, type, …)

Quand versionner une représentation ?

Les changements compatibles correspondront donc à un changement mineur, les changements incompatibles seront des changements majeurs. Ce sont ces derniers qui obligent une montée de version du service. On changera donc de version lorsque qu’un changement de comportement du service entraîne :

  • un changement incompatible
  • la création d’un lien vers une nouvelle ressource
  • la dépréciation d’une ressource.

Le versioning en REST

Il existe plusieurs moyens de versionner.

Le versioning via URL

La première réponse à ce problème de versioning a été de rajouter un paramètre identifiant la version dans l’URL de la ressource.

Par exemple la version 1 d’une ressource album avec une représentation XML :

1
2
3
4
5
6
GET http://.../v1/albums/12133
 
<album>
	<title>The dark side of the moon</title>
	<artist>The pink floyds</artist>
</album>

Et la version 2 de la même ressource album avec une représentation XML aussi :

1
2
3
4
5
6
7
8
9
10
11
GET http://.../v2/albums/12133
 
<album>
	<title>The dark side of the moon</title>
	<artists>
		<artist>Roger Waters</artist>
		<artist>David Guilmour</artist>
		<artist>Nick Mason</artist>
		<artist>Rick Wright</artist>
</artists>		
</album>

Ce versioning est inspirée du modèle SOAP où la version est portée dans l’adressage du service. A chaque nouvelle version d’un service on définie une nouvelle URL et un nouveau schéma WSDL associé.

Cette approche ne nous satisfait pas sur plusieurs points :

Rupture avec les principes REST :

La plus importante est que cette approche est en rupture avec un des principes de REST : une ressource est identifiée par une URI sous entendu durable et stable dans le temps
L’introduction de la version dans l’URI implique de définir une URI différente par version de la ressource maintenue par le serveur.
Dans l’exemple précédent deux URI permettent d’accéder à la ressource Album.

De plus cette approche donne l’impression aux utilisateurs que l’album dans une version de l’API est une ressource différente du même album dans une autre version. Hors, ce qui a changé ici n’est pas la ressource mais une de ces représentations (la représentation au format XML pour être complet).

Exposition de paramètres techniques :

Cette approche rend visible directement au client une partie de la complexité de la gestion des versions de l’API.
En effet, nous faisons apparaître dans l’URI un paramètre purement technique, le numéro de version, alors que l’URI représente la dénomination fonctionnelle et unique de la ressource.

Best practice REST

« Content negotiation »

Une autre approche qui s’appuie sur le mécanisme de négociation de contenu du protocole http réponds entre autre à ces limitations. Revenons en deux mots sur la négociation de contenu : ce mécanisme permet à un client d’une ressource web de spécifier quelle(s) représentation(s) il préfère obtenir.

Techniquement parlant cette information se trouve dans la requête http dans une entête appelée Accept header. Voici un exemple de requête avec négociation de contenu :

1
2
3
GET http://... /albums/12133
Accept : text/xml, application/json 
…

Le serveur répondra de préférence un fragment XML, s’il le gère, une structure de données JSON dans le cas contraire.
Les types définis dans l’Accept header peuvent être soit des media types standardisés soit des types personnalisés.

Versioning par Accept

Selon la philosophie REST le client spécifie dans sa requête la représentation de la ressource à laquelle il souhaite accéder. Ceci est fait au travers du mécanisme de négociation de contenu explicité ci-dessus.

Ce mécanisme permet donc de véhiculer les informations liées aux différentes versions de l’API. Le client spécifie dans le Accept Header quelles sont les versions d’une représentation d’une ressource qu’il souhaite avoir, par ordre de préférence.

La gestion des versions de service REST au travers de ce mécanisme a un prérequis : l’introduction de media types non standard.

Si nous reprenons l’exemple du versioning précédent les types créés seront :

  • application/vnd.octo.music-v1+xml associé au schéma XML de la version 1 de la représentation XML de l’album
  • application/vnd.octo.music-v2+xml associé au schéma XML de la version 2 de la représentation XML de l’album

Par exemple la requête pour accéder à la version 1 de la ressource album avec une représentation XML :

1
2
3
4
5
6
7
GET http://.../albums/12133
Accept : application/vnd.octo.music-v1+xml 
 
<album>
	<title>The dark side of the moon</title>
	<artist>The pink floyds</artist>
</album>

Et la requête pour accéder à la version 2 de la ressource album avec une représentation XML :

1
2
3
4
5
6
7
8
9
10
11
12
GET http://.../albums/12133
Accept : application/vnd.octo.music-v2+xml 
 
<album>
	<title>The dark side of the moon</title>
	<artists>
		<artist>Roger Waters</artist>
		<artist>David Guilmour</artist>
		<artist>Nick Mason</artist>
		<artist>Rick Wright</artist>
</artists>		
</album>

Conclusion

La principale solution apportée au problème de versioning de services REST a été d’introduire le numéro de version dans l’URI. Nous lui préférons la solution utilisant la négociation de contenu et le versioning de représentations plus que ressources.
Ceci pour plusieurs raisons :

  • Le versioning via URI enfreint un des principes fondamentaux de REST : Une URI  Une ressource
  • Dans la philosophie REST les échanges sont pilotés par le client, c’est le client qui sait quelle représentation il peut manipuler. Ceci autant pour le format des données (xml, json, html, …) que pour la version de ces données. Ces deux cas doivent être traités au travers du même mécanisme : la négociation de contenu et le champ Accept Header.
  • La négociation de contenu permet au client de définir les versions de représentation qu’il souhaite recevoir par ordre de préférence
  • Les principaux framework REST offrent des fonctionnalités natives de gestion de contenu coté client et serveur

Dans un article futur nous pourrons décrire l’architecture technique/applicative impliquée par la gestion de versions via Accept Header. Nous pourrons ainsi répondre aux questions : Comment mettre à disposition une nouvelle version d’un service ? Comment supporter coté serveur deux versions majeures d’un service en même temps ? Comment connaître les versions utilisées par les clients du service ?

Benoît Guillou & Benjamin Magnan

Mots-clés: , ,

12 commentaires pour “Versioning de services REST”

  1. Merci pour l’article, tres clair.

    Y’a-t-il des desavantages ou des limitations avec ce « Versonning par Accept » ?

  2. Un peu de discussion…
    - la chaîne de caractère dans ton en-tête peut ne passer passer un proxy un peu fasciste,
    - c’est intestable automatiquement par un navigateur,
    Et qui dit changement de version dit un peu de boulot côté consommateur de ton webservice.
    Dans ce cas, pourquoi ne pas carrément changer le nom du WS?
    Si on reprend ton exemple:
    GET http://…/albums/12133
    Pour la v1
    Et pour la v2:
    GET http://…/albumsAndArtists/12133

  3. Article intéressant. La remarque de Gabriel est pertinente également. C’est pourquoi il est envisageable d’identifier la représentation à envoyer au client via le User-Agent qu’il envoie. Un proxy « un peu fasciste » :-) laissera sans doute mieux passer un User-Agent, plus sensible au changement, qu’un en-tête peu commun.
    ex :
    GET http://…/albums/12133
    User-Agent: client-v1
    => Retourne la représentation v1

    GET http://…/albums/12133
    User-Agent: client-v2
    => Retourne la représentation v2

    Ainsi, c’est toujours le client qui est porteur de la version de contenu avec laquelle il est compatible. La montée de version nécessite néanmoins une modification du paramètre user-agent du client http utilisé (tout comme pour la solution avec négociation de contenu cela dit).

  4. Quelle classe Georges !
    Le User Agent est en effet un autre moyen de préciser la version du service à attaquer sans triturer l’URL de la ressource. Le champs Accept reste dans l’absolu l’attribut d’entête HTTP à privilégier.

    Pour ce qui est de ta remarque Gabriel ‘intestable par le navigateur’, ce n’est pas structurant dans la manière de penser les services REST. La testabilité du navigateur n’est pas la killer feature de REST (surtout si tu ne testes que les GET). De plus avec un petit about:config tu peux tout à fait modifier ce champs ACCEPT. A noter que le plugin Poster de Firefox permet d’envoyer n’importe quel type de requête et donc indispensable si tu souhaites tester un service à partir de ton navigateur.

    La solution « http://…/albumsAndArtists/12133″ retombe dans les travers d’un v2 dans l’url. De plus, tu changes ici complètement ton contrat de service : la ressource album n’existe plus et tu crées une nouvelle ressource albumsAndArtists.

  5. Merci pour ces posts, peut on pousser la reflexion plus loin? Pouvez vous également expliquer comment vous faites vivre les 2 versions de service coté serveur ?
    Est-ce qu’elles sont cote à cote dans la meme webapp ? Est-ce qu’on fait une 2e webapp? Est-ce qu’on sort la machine de guerre avec un bus qui transforme le format V1 en V2 ?
    Pouvez vous svp faire part de vrais retours d’experience pour certains cas?

    Merci encore
    Gilles S

  6. C’est l’autre sujet, très connexe certes, du multiversion côté implem; j’ai des exemples soap ou rpc propriétaire à vous soumettre, peut être sont ils valides pour rest également? (hypothèse peut être fragile)

    Première approche effectivement : même webapp; vu dans un système de réservation transport : 1 seul code qui supporte d’être interrogé de 2 façons différentes, avec des bon vieux If organisés aux bon endroits (= pas trop!), et pleins de tests de non régression.

    En l’occurrence, le système offrait des fonctionnalités récentes aux clients de l’ancienne API (ça ne changeait pas les apis des request ni des responses), donc le partage de code entre les 2 versions était nécessaire, et il avait été jugé plus facile de faire la maintenance sur un code unique.

    Pas vu mais envisageable en théorie : déployer 2+1 webapp sur le cluster, la 3ième minuscule webapp contenant le code de routage côté serveur selon la version; pour le monde java, appeler le dispatcher fourni par le servlet context (me semble t il), en tout cas celui qui est global, qui permet de basculer vers n’importe quel autre contexte url du serveur d’app, sans revenir vers le client.

    Enfin, il y a effectivement l’approche de transformation de message par un intermédiaire type ESB; les grands fournisseurs de réservation fonctionnent beaucoup comme ça (encore une fois, en approche message proche rpc, mais j’ai l’impression que le sujet du multiversion côté implem est orthogonal, non ?)

  7. A première vue, je ne suis pas vraiment fan d’un module web « routeur ».
    Je ne pense pas que ce soit de la responsabilité d’un serveur d’applications que de router une requête cliente vers une application web. A moins évidemment que ce choix soit justifié. C’est à dire par exemple si la décision de routage est basée sur des règles complexes nécessitant un traitement ou l’accès à des ressources ou des services particuliers…
    Dans le cas cité par Dominique (déploiement de 2 applis web pour 2 versions différentes), la décision de routage s’effectue à partir de l’analyse d’entêtes HTTP. Certes, c’est au niveau applicatif que cela se passe, mais notre pauvre serveur d’applications a sans doute autre chose à faire que de s’occuper de ça.
    Je refilerai plutôt le boulot à un reverse proxy, qui peut de façon plus efficace décider de router les requêtes vers la bonne version déployée sur notre besogneux serveur d’application, délestant ainsi ce dernier.

    C’est un avis tout à fait personnel :)

    C’est un choix qui dépend largement du domaine fonctionnel : fréquences d’apparition de nouvelles fonctions et/ou d’évolutions, etc… Quoiqu’il en soit, il arrive un moment où la mise à disposition et le maintien de trop de versions d’un même service devient trop complexe. D’où la nécessité sans doute de marquer certains services comme « dépréciés », comme une manière de prévenir les clients que le service est amené à ne plus être supporté, et ainsi à les amener à upgrader vers une version plus récente du client rest.

    Le sujet est vaste…

  8. [...] possibilités de versionning plus simple puisque la signature sera [...]

  9. 2 petites remarques sur ce vieil article :

    - Concernant la gestion de version via paramètre Header « Accept », c’est ce que préconise la norme HTTP ( http://tools.ietf.org/html/rfc2616#section-14.1 )
    Mais la norme préconise plutôt l’utilisation de la partie « accept-extension ». Ce qui donnerait dans l’exemple du service :

    GET http://…/albums/12133
    Accept : application/vnd.octo.music+xml;level=1

    Apache semble d’ailleurs prendre en compte ce paramètre :
    - « Sélectionner les variantes possédant le paramètre de média « level » le plus élevé (utilisé pour préciser la version des types de média text/html). »
    http://httpd.apache.org/docs/current/content-negotiation.html
    - « level an integer specifying the version of the media type. For text/html this defaults to 2, otherwise 0. »
    http://httpd.apache.org/docs/2.2/mod/mod_negotiation.html

    - pour répondre à « c’est intestable automatiquement par un navigateur », si des solution comme l’extension « Poster » n’est pas envisageable, on peut envisager de mettre en œuvre le principe de surcharge via query String de la même manière que « X-HTTP-Method-Override »

    A part ça … concernant « Dans un article futur nous pourrons décrire l’architecture technique/applicative impliquée par la gestion de versions via Accept Header. » … je n’ai pas trouvé l’article :)

  10. [...] Versioning des Services REST. Sur le Blog Octo, Benjamin Magan nous propose une stratégie pour le versioning des Services REST. La technique proposée est élégante et repose sur la négociation du contenu et l’entête « Content-Type ». Le client indique à l’aide cet entête, non seulement le format de représentation des données (XML, JSON, …) mais aussi la version du service qu’il utilise. De cette manière, l’URL reste inchangée ce qui est important avec des services REST. [...]

  11. Pour continuer la discussion, ils en parlent ici aussi : http://www.la-grange.net/2011/07/14/api-version

  12. Encore un autre article sur le sujet :
    http://www.baeldung.com/rest-versioning

    Et pour continuer le référencement, un petit « loopback » :
    http://blog.octo.com/versioning-des-services-principes-et-elements-darchitecture%E2%80%A6/

Laissez un commentaire