Quantcast
Channel: Publicis Sapient Engineering – Engineering Done Right
Viewing all articles
Browse latest Browse all 1865

POST vs. PUT : la confusion

$
0
0

Aujourd’hui, de plus en plus d’applications web disposent d’une API. La pratique de SOAP étant jugée trop glissante (surtout avec les interfaces liquides), c’est le REST qui se répand.

Ce style d’architecture reposant sur HTTP, comprendre la norme HTTP est essentiel.

Cependant, les mauvais usages et autres anti-patterns sont à l’affût, prêts à se jeter sur les concepteurs et développeurs d’API. Dans cet article, nous nous attacherons à démêler une confusion entre PUT et POST.

Comme remarqué par John Calcote, beaucoup ont été tentés de faire un mapping naïf entre CRUD et les verbes HTTP :

  • Create = PUT
  • Retrieve = GET
  • Update = POST
  • Delete = DELETE

Et une variante très répandue :

  • Create = POST
  • Update = PUT

Certains seront peut-être chagrinés d’apprendre que jusqu’en 2010, il n’y avait que 3 verbes HTTP dédiés au CRUD, et que POST n’a jamais fait partie et ne fera jamais partie d’entre eux.

Posez vos boucliers, je n’ai pas dit que nous ne pouvions pas créer ou mettre à jour une entité avec POST ! Mais comme dans HTTP, et à fortiori dans une API RESTful, la sémantique est très importante et a un impact sur le design de l’API, POST peut avoir le rôle de création , mais n’est pas présenté comme tel. Nous allons devoir y regarder de plus près.

Les mauvaises pratiques

Voici la liste non exhaustive des mauvaises pratiques autour de PUT et POST :

PUT pour modifier uniquement, POST pour créer

Erreur commise dans le but de « clarifier » les rôles respectifs de PUT et POST, afin que les « débutants » comprennent vite leur API. Également commise par ceux qui trouvent POST bien pratique pour créer, et qui ont cantonné PUT à un rôle par défaut (par méconnaissance du verbe PUT ?). Un inconvénient de cette pratique, si on ne connait pas le récent verbe PATCH, est qu’on ne peut pas faire de mise à jour partielle via POST alors que c’est très pratique et largement toléré. Nous verrons plus bas que si PUT n’est pas utilisé pour créer, cela ne doit résulter que d’une contrainte.

POST utilisant l’URI de l’entité à créer

Probablement le plus grave. Ceci constitue à la fois une violation de la définition de POST et une ignorance totale du rôle de PUT. Dans le cas où POST crée une ressource, le corps de la requête contient soit l’entité, soit des informations permettant de la créer, mais l’URI est toujours une URI tierce capable de traiter la demande.

Exemple de comportement non conforme :
POST /clients/123
> ...
> ...
< 201 Created
> GET /clients/123
< 200
< ...
< ...

Exemple de comportement conforme :
> POST /clients/create
> ...
> ...
< 201 Created
< Location: /clients/123
> GET /clients/123
< 200 OK
< ...
< ...

POST qui crée une entité sans renvoyer le header « Location »

Le code retour 201 étant facultatif mais conseillé ; hé oui, où est l’entité créée ou modifiée ? En renvoyant le header Location, en réponse à un POST supposé créer une ressource, je permets au client de connaître son identifiant.

PUT qui renvoie un header Location

C’est superflu, vu qu’une requête PUT se fait sur l’URI de l’entité à créer. Si par exemple je devais compléter cet échange :
> GET /clients/123
< 404
> PUT ???
> ...
< 201
> GET /clients/123
< 200
< ...

Les points d’interrogation ne peuvent être remplacés que par « /clients/123″, du coup pas besoin de header Location.

PUT partiel

On doit fournir une entité complète dans le corps d’une requête PUT

POST omnipotent, PUT inutilisé voire inutilisable

Cela revient à supprimer la sémantique de PUT et POST, et à la faire porter entièrement à l’URI et à ses paramètres. Un peu comme SOAP, un peu comme les débuts erratiques du web où toutes pages d’un site étaient servies par /servlet/Toto?page=truc&action=toutgerer&jenpasse&desmeilleures

POST pour créer alors qu’on connait l’identifiant de l’entité à créer

Si l’identifiant de l’entité à créer est prévisible, PUT est plus approprié car nous avons alors les deux conditions nécessaires pour faire un PUT :

  • avoir une entité complète (création)
  • avoir l’URI de l’entité (grâce à l’identifiant)

Mais alors comment PUT et POST sont-ils conçus pour fonctionner ?

PUT

Regardons la norme HTTP. D’après la RFC 2616 (HTTP methods), PUT sert à

Créer une entité :
  • L’URI doit être celle de l’entité à créer
  • Si l’entité dans la requête est acceptée et créée côté serveur, le serveur doit renvoyer un code 201 (created).
  • Si l’entité peut avoir plusieurs URIs (exemple : /script/v1.2 ou /script/current), le serveur peut donc définir ces URIs alternatives comme pointant sur la nouvelle entité
Modifier une entité :
  • Si une entité avec la même URI que celle de la requête existe déjà, l’entité de la requête devrait la remplacer.
  • Ce n’est pas une création, le code retourné par le serveur en cas de succès doit être 200 (OK) ou 204 (No content). En effet, en pratique un PUT va très rarement générer une réponse avec un contenu.
Dans les deux cas :
  • Une incompréhension des header Content-* de la part du serveur doit générer une erreur 501 (not implemented) car il n’a pas le droit de les ignorer. Les autres cas d’erreur devraient également donner un code cohérent, voire un message d’erreur cohérent.
  • L’URI du PUT est celle de l’entité à créer/mettre à jour.
  • Le corps de la requête PUT doit contenir l’entité complète.
  • La redirection est possible, mais doit être permanente (code 301). Libre au client de suivre la redirection, mais s’il s’arrête là le PUT n’est évidemment pas effectif. Ce côté définitif de la redirection est nécessaire du fait que l’URI doit désigner l’entité, et non pas un processus ou action qui prendrait en charge la requête. On ne redirige donc que si on veut indiquer un changement d’URI. Il faut donc s’attendre à ce qu’une future requête accède directement à l’URI indiquée dans le précédent 301.

Détail qui a son importance, la réponse à un PUT n’est pas cachable car dépendant intrinsèquement du contenu de la requête ! Vu que la réponse est vide dans 99% des cas, ceci n’est qu’un détail logique.

Enfin, deux requêtes PUT identiques (même contenu, même URI) laissent le système dans le même état que si une seule avait été lancée (idempotence). Un détail ne devra pas nous échapper : on doit répondre 200 ou 204 à la seconde si on a répondu 201 à la première ; si James Bond ne meurt que deux fois, une entité n’est elle, créée qu’une fois.
> GET /clients/123
< 404
> PUT /clients/123
> toto
< 201
> GET /clients/123
< 200
< toto
> PUT /clients/123
> toto
< 204
> GET /clients/123
< 200
< toto
> PUT /clients/123
> poum
< 204
> GET /clients/123
< 200
< poum

Ci-dessus, nous voyons que le 201 n’est renvoyé que la première fois, à la création. 200 aurait été acceptable à la place de 204, mais 204 signale explicitement que la réponse n’a pas de corps, alors qu’avec un 200 le client le découvre.

Conclusion : PUT est bien un double verbe CRUD, capable de créer ou de modifier en intégralité l’entité désignée par l’URI. En pratique, PUT peut être écarté comme verbe pour la création car notre application ne permettra pas (politique de sécurité, contrainte technique ou contrainte fonctionnelle) à un client de connaître à l’avance son URI.

POST

Toujours d’après la RFC 2616, POST sert à :

  • annoter des ressources existantes
  • poster un message au sens « forum » du terme
  • envoyer des données à un processus qui saura les prendre en charge
  • ajouter des données dans une base de donnée

Vous trouvez ça flou ? La RFC confirme votre impression : c’est le serveur qui détermine ce que nous devons faire d’un POST. Donc, pas de sémantique forte associée à ce verbe.

Comme pour le PUT, le POST doit renvoyer le statut 200 si la réponse contient une entité, 204 si elle est vide.

Maintenant, vous aurez peut-être une question : la création en POST, nous n’en avons pas parlé ! Ai-je raté quelque chose ?

Pas de panique : il y a bien une petite phrase pour dire que si une entité est créée suite à une POST, la réponse a le statut 201 et devrait également indiquer dans le header Location l’uri de la nouvelle entité.

On voit donc que la création d’entité n’est qu’un rôle annexe de POST, il n’y a donc aucune raison de l’introniser « verbe CRUD pour la création » !

Le problème pratique

Avec de telles définitions, les concepteurs d’API se sont vite trouvés embarrassés : d’un côté, un PUT qui permet create/update mais avec une entité complète ; de l’autre, un POST dont la sémantique floue permet de tout faire. Inévitablement, POST s’est retrouvé utilisé pour les créations et mises à jour avec des informations partielles. Comme on ne peut pas toujours connaître à l’avance l’URI de l’entité à créer, POST a donc pris fréquemment le rôle de create/update : on demande une création avec les informations postées, et la nouvelle URI est dans la réponse.

Ce dernier point marque une carence du protocole : il n’y a pas de moyen, en HTTP, de mettre à jour une entité connue, en la désignant directement par son URI (comme avec PUT), avec juste le différentiel d’informations. Avec POST, il faut passer par une URI tierce, qu’on appellera un handler, une action, un processus, etc. qui lui prendra la demande en charge et inventera un sens contextualisé pour POST. Et ce même si nous connaissons l’URI de l’entité à modifier ! Il n’y a pas moyen… enfin c’était le cas jusqu’en 2010.

Heureusement, à partir de cette année là, la RFC5789 a introduit un nouveau verbe : PATCH. Le gros avantage, c’est qu’on peut désormais « patcher » une entité, c’est à dire la modifier partiellement quand on connait son URI. L’inconvénient, d’après moi, c’est que POST perd un rôle qui était clair et se retrouve à prendre « le reste », ce que les autres verbes ne prendront pas par soucis de respect de leur sens.

Alors si tout cela est une histoire de sémantique, pourquoi n’imposerions-nous pas les rôles des différents verbes HTTP pour que nos APIs soient plus claires ?

Le rôle central de la sémantique

Dans le protocole HTTP, la sémantique a un rôle déterminant sur la manière dont les différents éléments (matériels ou logiciels) doivent agir.

La respecter implique de :

  • s’assurer que les autres éléments soient cohérents dans leur manière d’agir et que nous n’obtiendrons pas des résultats aberrants.
  • permettre que n’importe quel élément inséré qui touchera à des requêtes ou réponses HTTP puisse agir de manière performante et optimisée. La RFC fourmille de suggestions sur ce que devraient faire ou ne pas faire les proxies, par exemple.

La discoverability est un aspect intéressant des APIs REST, qui se base sur le formalisme des éléments du protocole.

La discoverability est la qualité d’une API à pouvoir être découverte par un client à partir d’indices sémantiques. Si vous avez entendu parler de « web sémantique », vous avez une bonne idée de comment permettre la discoverability dans une API. L’idée est de découvrir les URIs de l’API à partir d’une ressource référence et de leur donner des rôles

Comment ? Je vous conseille de lire REST : Richardson Maturity Model qui lui-même fait référence à Richardson Maturity Model (par Martin Fowler). Vous y trouvez, au niveau 3 du Richardson Maturity Model, des exemples concrets qui montrent comment dans REST, une ressource permet d’en découvrir d’autres.

La sémantique complète n’est donc pas portée par les seuls verbes : les headers la modulent, la sémantique des liens (leur rôle) la précisent.

Les verbes HTTP donnent donc un sens très formel, qui devrait être précisé. Mais comment créer une sémantique applicative cohérente si le sens premier des verbes n’est pas respecté ?

Si vos APIs ne sont pas RESTful (ce qui est le cas de 98% des APIs qui se disent RESTful), il reste nécessaire de respecter le minimum de sens porté par les verbes HTTP pour tirer parti au maximum de l’architecture induite.

Le sens minimaliste intrinsèque des verbes HTTP (indépendamment de la sémantique donnée par les ressources) est actuellement un standard respecté largement : le simple fait de naviguer sur le web, c’est utiliser une « API » RESTful.

Si le respect du standard semble déjà une bonne raison, nous pourrions également ajouter les conventions qui rendent le développement, la maintenance et la communication autour d’une application possible : vous viendrait-il à l’esprit de créer une classe Java avec des getters dont le préfixe est « set » ou des setters dont le préfixe est « get » ? Pourtant me direz-vous, il suffit de le préciser dans la javadoc… Avec les verbes HTTP, c’est pareil. Il est possible de faire n’importe quoi et de le documenter, mais mieux vaut garder les conventions établies par les RFCs et déjà respectées par ailleurs.

Impact sur les APIs HTTP

Maintenant que nous sommes convaincus que la différence PUT/POST n’est pas une simple vue de l’esprit, que faire ?

Nous distinguerons les simples APIs HTTP, qui ont l’inconvénient d’avoir le client lié à l’organisation des ressources sur le serveur, et les APIs RESTful, qui bénéficient du découplage permis par les hypermedias.

APIs HTTP

Si vous avez des API exposées à l’extérieur qui ne respecteraient pas la norme, voyez si vous pouvez assurer une migration en profitant d’un refactoring ou d’une réorganisation des applicatifs clients. Si aucune occasion se présente, il est peu probable que votre client accepte de changer la manière de consommer votre API juste parce que vous avez ignoré la RFC.

Par contre, n’hésitez pas à introduire le nouveau formalisme pour toute nouvelle ressource exposée, même dans des API existantes. Vous éduquerez les développeurs au plus tôt sur la nouvelle manière de faire sans perturber les consommateurs des ressources existantes.

APIs RESTful

Si vous avez opté pour du RESTful, vous devriez rencontrer moins de problèmes. Même si le découplage permis par l’architecture REST vous permet de changer pas mal de chose côté serveur sans perturber le client, voyez au cas par cas si

  • le client consomme bien votre API de manière RESTful
  • la ressource permet de changer facilement le verbe (exemple : la méthode pour accéder à une ressource liée est précisée dans la ressource parente)

J’ai conscience que peu de gens imaginent la flexibilité réelle d’une API RESTful, et qualifient à tort une simple API HTTP de RESTful, je vous conseille donc la visualisation de Building Hypermedia APIs with HTML (Jon Moore). Cette vidéo qui sort largement du cadre de la confusion PUT/POST nous montre une mise en œuvre d’une API RESTful. Une autre vidéo de l’auteur montrant la mise en œuvre d’une API RESTful avec des documents XHTML a été supprimée de Viméo, mais le court article Thoughts on hypermedia apis donne l’esprit original. Quel que soit la vidéo, on y voit comment REST permet de dé-corréler le client et la structure des ressources coté serveur.

Conclusion

Nous avons donc vu que le mapping CRUD ⟷ verbes HTTP n’existait pas, et que PUT et POST avaient chacun leurs rôles bien définis. Bien qu’en pratique il sera plus facile de créer une ressource avec POST, il faudra bien veiller à ce que le POST crée une ressource liée dont il retournera l’uri (Header Location) et à ne pas oublier qu’implémenter la création avec PUT reste possible. Si créer avec PUT est possible, faites-le car c’est plus naturel (URI ressource ressource créée = URI de la requête PUT).

Nous avons vu que le respect de la distinction PUT/POST avait une influence sur la gestion des flux réseau, et sur le design des APIs. Également, le rôle de POST est beaucoup plus flou et « free style » depuis l’apparition de PATCH. L’idéal serait donc d’utiliser PATCH pour les mises à jour partielles, PUT pour les créations et mises à jour complètes, et POST pour les créations complètes lorsque PUT n’est pas possible ainsi que tout ce qui n’est pas faisable avec les autres verbes. Le mapping CRUD ↔ verbes HTTP semble inutile, car en suivant les RFCs on aboutit toujours à quelque chose de juste. Il suffit juste de s’interroger, avant chaque usage de POST, si un autre verbe ne collerait pas mieux. Et si ce n’est pas le cas, et bien… utilisons POST !

Enfin nous avons entraperçu un sujet parallèle, à savoir que quasiment personne prétendant faire du REST n’en fait vraiment, alors que les avantages sont évidents (découplage libérant le serveur de contraintes empêchant l’évolution et permettant au client de ne pas être impacté par chaque ajustement côté serveur).


Viewing all articles
Browse latest Browse all 1865

Trending Articles