Comment une API REST doit-elle gérer les demandes PUT adressées à des ressources partiellement modifiables?

46

Supposons qu'une API REST, en réponse à une GETrequête HTTP , renvoie des données supplémentaires dans un sous-objet owner:

{
  id: 'xyz',
  ... some other data ...
  owner: {
    name: 'Jo Bloggs',
    role: 'Programmer'
  }
}

De toute évidence, nous ne voulons pas que quiconque puisse PUTsoutenir

{
  id: 'xyz',
  ... some other data ...
  owner: {
    name: 'Jo Bloggs',
    role: 'CEO'
  }
}

et que cela réussisse. En effet, nous n'allons probablement même pas mettre en place un moyen pour que cela réussisse potentiellement, dans ce cas.

Mais cette question ne concerne pas seulement les sous-objets: que faut-il généralement faire avec des données qui ne devraient pas être modifiables dans une requête PUT?

Devrait-il être tenu de manquer de la demande PUT?

Devrait-il être jeté silencieusement?

Devrait-il être vérifié et, si elle diffère de l'ancienne valeur de cet attribut, renvoyer un code d'erreur HTTP dans la réponse?

Ou devrions-nous utiliser les correctifs JSON RFC 6902 au lieu d’envoyer l’ensemble du code JSON?

Robin Green
la source
2
Tout cela fonctionnerait. Je suppose que cela dépend de vos besoins.
Robert Harvey
Je dirais que ce principe de moindre surprise indiquerait qu'il devrait être absent de la demande PUT. Si ce n'est pas possible, vérifiez si c'est différent et renvoyez-le avec un code d'erreur. La mise au rebut silencieuse est pire (l’envoi de l’utilisateur s’attend à ce que cela change et vous lui dites "200 OK").
Maciej Piechotka
2
@MaciejPiechotka le problème avec ceci est que vous n'obtenez pas à utiliser le même modèle sur le put ou sur l'insert ou obtenir etc, je préférerais que le même modèle soit utilisé et qu'il y ait simplement des règles d'autorisation de champs un champ qu'ils ne devraient pas changer, ils récupèrent un 403 interdit, et si plus tard, une autorisation leur est accordée, ils obtiennent un 401 non autorisé s'ils ne sont pas autorisés
Jimmy Hoffa
@ JimmyHoffa: Par modèle, vous entendez le format de données (car il pourrait être possible de réutiliser le modèle dans la structure MVC Rest en fonction de son choix, le cas échéant - OP n'en a pas mentionné)? J'irais avec la possibilité de découverte si je n'étais pas contraint par le cadre et que l'erreur précoce est légèrement plus facile à mettre en œuvre que de la vérifier, mais il est plus facile de la mettre en œuvre que de vérifier le changement (ok, je ne devrais pas toucher au champ XYZ). Dans tous les cas, le rejet est pire.
Maciej Piechotka

Réponses:

46

Aucune règle, ni dans la spécification W3C ni dans les règles non officielles de REST, n'indique qu'un PUTdoit utiliser le même schéma / modèle que celui correspondant GET.

C'est bien s'ils sont similaires , mais il n'est pas inhabituel PUTde faire les choses légèrement différemment. Par exemple, j'ai vu beaucoup d'API qui incluent une sorte d'identifiant dans le contenu renvoyé par un GET, par souci de commodité. Mais avec a PUT, cet ID est déterminé exclusivement par l'URI et n'a aucune signification dans le contenu. Toute identification trouvée dans le corps sera ignorée en silence.

REST et le Web en général sont fortement liés au principe de robustesse : "Soyez conservateur dans ce que vous faites [envoyez], soyez libéral dans ce que vous acceptez." Si vous êtes d'accord philosophiquement avec cela, la solution est évidente: ignorez les données non valides dans les PUTdemandes. Cela s'applique à la fois aux données immuables, comme dans votre exemple, et aux absurdités réelles, par exemple les champs inconnus.

PATCHC'est potentiellement une autre option, mais vous ne devriez pas l'implémenter à PATCHmoins de supporter des mises à jour partielles. PATCHsignifie seulement mettre à jour les attributs spécifiques que j'inclus dans le contenu ; cela ne signifie pas remplacer l'entité entière mais exclure certains champs spécifiques . Ce dont vous parlez en réalité n’est pas vraiment une mise à jour partielle, c’est une mise à jour complète, idempotente et tout, c’est simplement que cette partie de la ressource est en lecture seule.

Une bonne chose à faire si vous choisissez cette option serait de renvoyer un 200 (OK) avec l'entité mise à jour dans la réponse, afin que les clients puissent clairement voir que les champs en lecture seule n'ont pas été mis à jour.

Il y a certainement des gens qui pensent autrement, que tenter de mettre à jour une partie en lecture seule d'une ressource devrait être une erreur. Cela peut être justifié, principalement parce que vous renverriez certainement une erreur si l' intégralité de la ressource était en lecture seule et que l'utilisateur tentait de la mettre à jour. Cela va certainement à l’encontre du principe de robustesse, mais vous pourriez le considérer comme plus "auto-documenté" par les utilisateurs de votre API.

Il existe deux conventions pour cela, qui correspondent toutes deux à vos idées originales, mais je les développerai davantage. La première consiste à empêcher les champs en lecture seule d'apparaître dans le contenu et à renvoyer un HTTP 400 (demande incorrecte) s'ils le font. Les API de ce type doivent également renvoyer un HTTP 400 s'il existe d'autres champs non reconnus / inutilisables. La seconde consiste à exiger que les champs en lecture seule soient identiques au contenu actuel et à renvoyer un 409 (conflit) si les valeurs ne correspondent pas.

Je n’aime vraiment pas le contrôle d’égalité avec 409 car il oblige invariablement le client à effectuer un GETtest afin de récupérer les données actuelles avant de pouvoir effectuer un contrôle PUT. Ce n'est tout simplement pas agréable et cela conduira probablement à de mauvaises performances, pour quelqu'un, quelque part. De plus, je n'aime vraiment pas 403 (interdit) car cela implique que la totalité de la ressource est protégée, pas seulement une partie de celle-ci. Donc, mon opinion est la suivante: si vous devez absolument valider au lieu de suivre le principe de robustesse, validez toutes vos demandes et renvoyez un 400 pour toutes celles comportant des champs supplémentaires ou non inscriptibles.

Assurez-vous que votre 400/409 / what comprend des informations sur le problème spécifique et sur la façon de le résoudre.

Ces deux approches sont valables, mais je préfère la première, conformément au principe de robustesse. Si vous avez déjà travaillé avec une grande API REST, vous apprécierez la valeur de la compatibilité ascendante. Si vous décidez de supprimer un champ existant ou de le rendre en lecture seule, il s'agit d'une modification rétrocompatible si le serveur ignore simplement ces champs et que les anciens clients fonctionnent toujours. Toutefois, si vous appliquez une validation stricte au contenu, celui-ci n’est plus compatible avec les versions antérieures et les anciens clients ne fonctionneront plus. Le premier signifie généralement moins de travail pour le responsable d'une API et ses clients.

Aaronaught
la source
1
Bonne réponse et voté. Cependant, je ne suis pas sûr d’être d’accord avec ceci: «Si vous décidez de supprimer un champ existant ou de le rendre en lecture seule, il s’agit d’une modification rétrocompatible si le serveur ignore simplement ces champs et les anciens clients continueront de fonctionner. " Si le client s'appuie sur ce champ supprimé / nouvellement en lecture seule, cela n'aura-t-il pas une incidence sur le comportement général de l'application? Dans le cas de la suppression de champs, je dirais qu'il est probablement préférable de générer explicitement une erreur plutôt que d'ignorer les données; sinon, le client n'a aucune idée que sa mise à jour, qui fonctionnait auparavant, échoue maintenant.
rinogo
Cette réponse est fausse. pour 2 raisons de la RFC2616: 1. (section 9.1.2) PUT doit être indépendant. Mettez plusieurs fois et cela produira le même résultat que de ne mettre qu'une fois. 2. Le get à une ressource doit renvoyer l'entité mise si aucune autre demande n'a été faite pour changer la ressource
brunoais
1
Que faire si vous ne faites la vérification d’égalité que si la valeur immuable a été envoyée dans la demande? Je pense que cela vous donne le meilleur de deux mondes; vous ne forcez pas les clients à effectuer une opération GET et vous leur signalez tout de même que quelque chose ne va pas s'ils envoient une valeur non valide pour une immuable.
Ahmad Abdelghany
Merci, la comparaison approfondie que vous avez faite dans les derniers paragraphes et tirée de l’expérience est exactement ce que je cherchais.
dhill
9

Puissance idem

À la suite de la RFC, un PUT devrait fournir un objet complet à la ressource. La raison principale en est que PUT devrait être idempotent. Cela signifie qu'une demande répétée doit donner le même résultat sur le serveur.

Si vous autorisez les mises à jour partielles, cela ne peut plus être idem-puissant. Si vous avez deux clients. Client A et B, le scénario suivant peut évoluer:

Le client A obtient une image à partir d'images de ressources. Ceci contient une description de l'image, qui est toujours valide. Le client B met une nouvelle image et met à jour la description en conséquence. La photo a changé. Le client A voit, il n'a pas à changer la description, car c'est comme il veut et ne met que l'image.

Cela conduira à une incohérence, les mauvaises métadonnées attachées à l'image!

Encore plus ennuyeux, tout intermédiaire peut répéter la demande. Dans le cas où il décide d'une manière ou d'une autre, le PUT a échoué.

La signification de PUT ne peut pas être changée (bien que vous puissiez en abuser).

Autres options

Heureusement, il y a une autre option, c'est PATCH. PATCH est une méthode qui vous permet de mettre à jour partiellement une structure. Vous pouvez simplement envoyer une structure partielle. Pour des applications simples, c'est très bien. Cette méthode n'est pas garantie d'être idem puissant. Le client doit envoyer une demande sous la forme suivante:

PATCH /file.txt HTTP/1.1
Host: www.example.com
Content-Type: application/example
If-Match: "e0023aa4e"
Content-Length: 20
{fielda: 1, fieldc: 2}

Et le serveur peut répondre avec 204 (pas de contenu) pour signaler le succès. En cas d'erreur, vous ne pouvez pas mettre à jour une partie de la structure. La méthode PATCH est atomique.

L'inconvénient de cette méthode est que tous les navigateurs ne la prennent pas en charge, mais qu'il s'agit de l'option la plus naturelle dans un service REST.

Exemple de demande de correctif: http://tools.ietf.org/html/rfc5789#section-2.1

Json patcher

L'option json semble être assez complète et une option intéressante. Mais il peut être difficile à mettre en œuvre pour des tiers. Vous devez décider si votre base d'utilisateurs peut gérer cela.

Il est également un peu compliqué, car vous devez créer un petit interpréteur qui convertit les commandes en une structure partielle, que vous allez utiliser pour mettre à jour votre modèle. Cet interpréteur doit également vérifier si les commandes fournies ont un sens. Certaines commandes s'annulent. (écrivez fielda, supprimez fielda). Je pense que vous souhaitez signaler cela au client pour limiter le temps de débogage de son côté.

Mais si vous avez le temps, c'est une solution vraiment élégante. Vous devez encore valider les champs de cours. Vous pouvez combiner cela avec la méthode PATCH pour rester dans le modèle REST. Mais je pense que POST serait acceptable pour ici.

Aller mal

Si vous décidez d’utiliser l’option PUT, vous risquez un peu. Dans ce cas, vous devriez au moins ne pas supprimer l’erreur. L'utilisateur a une certaine attente (les données seront mises à jour) et si vous cassez cela, vous allez donner du temps à certains développeurs.

Vous pouvez choisir de signaler: 409 Conflict ou 403 Forbidden. Cela dépend comment vous regardez le processus de mise à jour. Si vous le voyez comme un ensemble de règles (centré sur le système), alors le conflit sera plus agréable. Quelque chose comme, ces champs ne peuvent pas être mis à jour. (En conflit avec les règles). Si vous le voyez comme un problème d’autorisation (centré sur l’utilisateur), vous devriez alors renvoyer interdit. Avec: vous n'êtes pas autorisé à modifier ces champs.

Vous devez toujours obliger les utilisateurs à envoyer tous les champs modifiables.

Une option raisonnable pour le faire est de le définir sur une sous-ressource, qui ne propose que les données modifiables.

Opinion personnelle

Personnellement, j'irais (si vous n'avez pas à travailler avec des navigateurs) pour le modèle PATCH simple, puis étendez-le plus tard avec un processeur de patch JSON. Cela peut être fait en différenciant les types MIME: Le type MIME de JSON Patch:

application / json-patch

Et json: application / json-patch

facilite sa mise en œuvre en deux phases.

Edgar Klerks
la source
3
Votre exemple d'idempotency n'a pas de sens. Soit vous modifiez la description, soit vous ne le faites pas. De toute façon, vous obtiendrez le même résultat à chaque fois.
Robert Harvey
1
Vous avez raison, je pense qu'il est temps d'aller se coucher. Je ne peux pas l'éditer. C'est plus un exemple sur la raison d'envoyer toutes les données dans une requête PUT. Merci pour le pointeur.
Edgar Klerks
Je sais que c'était il y a 3 ans ... mais savez-vous où dans la RFC, je peux trouver plus d'informations sur "PUT devrait fournir un objet complet à la ressource". J'ai vu cela mentionné ailleurs, mais j'aimerais voir comment cela est défini dans la spécification.
CSharper
Je pense que je l'ai trouvé? tools.ietf.org/html/rfc5789#page-3
CSharper