Quel code d'état HTTP à renvoyer si plusieurs actions se terminent par des statuts différents?

72

Je construis une API où l'utilisateur peut demander au serveur d'effectuer plusieurs actions dans une requête HTTP. Le résultat est renvoyé sous forme de tableau JSON, avec une entrée par action.

Chacune de ces actions peut échouer ou réussir indépendamment l’une de l’autre. Par exemple, la première action peut réussir, l'entrée dans la deuxième action peut être mal formatée et ne pas être validée, et la troisième action peut provoquer une erreur inattendue.

S'il y avait une demande par action, je retournerais les codes d'état 200, 422 et 500 respectivement. Mais maintenant, quand il n'y a qu'une seule demande, quel code d'état dois-je renvoyer?

Quelques options:

  • Toujours retourner 200 et donner des informations plus détaillées dans le corps.
  • Peut-être suivre la règle ci-dessus uniquement quand il y a plus d'une action dans la demande?
  • Peut-être retourner 200 si toutes les demandes aboutissent, sinon 500 (ou un autre code)?
  • Utilisez juste une requête par action et acceptez les frais supplémentaires.
  • Quelque chose de complètement différent?
Anders
la source
3
Votre question m'a fait penser à une autre: programmers.stackexchange.com/questions/309147/…
AilurusFulgens
7
Légèrement liées également: programmers.stackexchange.com/questions/305250/… (voir la réponse acceptée sur la séparation entre les codes d'état HTTP et les codes d'application)
4
Quel est l’avantage que vous obtenez en regroupant ces demandes? S'agit-il d'une logique métier, telle qu'une transaction sur plusieurs ressources, ou s'agit-il de performances? Ou autre chose?
Luc Franken
5
Ok, dans ce cas, je suggérerais fortement d’améliorer cette performance dans d’autres domaines. Essayez des solutions telles que l'interface utilisateur optimiste, le traitement par lots, la mise en cache, etc. avant d'implémenter cette complexité dans votre couche de gestion. Avez-vous une idée claire de l'endroit où vous perdez le plus de temps?
Luc Franken
4
... n'espérez pas que les gens examineront correctement ces statuts. La plupart des programmes ne vérifient que les plus courants et échouent ou se comportent mal s'ils obtiennent un code d'état inattendu. (Je me souviens qu'il y avait également une présentation à DefCon sur la protection de votre site contre les robots d'exploration en envoyant des statuts de sortie aléatoires que le navigateur ignore et indique simplement pourquoi les robots d'exploration considèrent parfois les erreurs comme telles et arrêtent donc d'explorer cette partie de votre site Web).
Bakuriu

Réponses:

21

La réponse courte et directe

Puisque la demande parle d'exécuter la liste des tâches (les tâches sont la ressource dont nous parlons ici), si le groupe de tâches a été déplacé vers l'exécution (c'est-à-dire quel que soit le résultat de l'exécution), il serait judicieux que le statut de la réponse sera 200 OK. Sinon, si un problème empêchait l'exécution du groupe de tâches, tel qu'un échec de la validation des objets de tâche , ou qu'un service requis n'était pas disponible, le statut de la réponse devrait indiquer cette erreur. Après cela, lorsque l'exécution des tâches commence, étant donné que les tâches à exécuter sont répertoriées dans le corps de la demande, je m'attendrais alors à ce que les résultats de l'exécution soient répertoriés dans le corps de la réponse.


La longue réponse philosophique

Vous rencontrez ce dilemme parce que vous vous détournez de ce pour quoi HTTP a été conçu. Vous ne l’interagissez pas pour gérer les ressources, vous l’utilisez plutôt comme moyen d’invocation de méthode à distance (ce qui n’est pas très étrange, mais fonctionne mal sans un schéma préconçu).

Compte tenu de ce qui précède et sans avoir le courage de transformer cette réponse en un long guide d’opinion, voici un schéma d’URI conforme à une approche de gestion des ressources:

  • /tasks
    • GET liste toutes les tâches, paginées
    • POST ajoute une seule tâche
  • /tasks/task/[id]
    • GET répond avec l'objet d'état d'une seule tâche
    • DELETE annule / supprime une tâche
  • /tasks/groups
    • GET liste tous les groupes de tâches, paginés
    • POST ajoute un groupe de tâches
  • /tasks/groups/group/[id]
    • GET répond avec l'état d'un groupe de tâches
    • DELETE annule / supprime le groupe de tâches

Cette structure parle de ressources, pas de quoi en faire. Ce qui se fait avec les ressources concerne un autre service.

Un autre point important à souligner est qu’il est conseillé de ne pas bloquer trop longtemps dans un gestionnaire de requêtes HTTP. Tout comme l'interface utilisateur, une interface HTTP doit être réactive - dans une échelle de temps inférieure de quelques ordres de grandeur (car cette couche traite des entrées / sorties).

Passer à la conception d'une interface HTTP qui gère strictement les ressources est probablement aussi difficile que de déplacer le travail d'un thread d'interface utilisateur lorsqu'un bouton est cliqué. Cela nécessite que le serveur HTTP communique avec d'autres services pour exécuter des tâches plutôt que de les exécuter dans le gestionnaire de demandes. Ce n'est pas une implémentation superficielle, c'est un changement de direction.


Quelques exemples d'utilisation d'un tel schéma d'URI

Exécuter une tâche unique et suivre les progrès:

  • POST /tasks avec la tâche à exécuter
    • GET /tasks/task/[id]jusqu'à ce que l'objet de réponse completedait une valeur positive tout en affichant l'état actuel / l'avancement

Exécution d'une tâche unique et en attente de son achèvement:

  • POST /tasks avec la tâche à exécuter
    • GET /tasks/task/[id]?awaitCompletion=trueUntil completeda une valeur positive (probablement un délai d'expiration, c'est pourquoi il devrait être mis en boucle)

Exécution d'un groupe de tâches et suivi des progrès:

  • POST /tasks/groups avec le groupe de tâches à exécuter
    • GET /tasks/groups/group/[groupId]jusqu'à ce que la completedpropriété d' objet de réponse ait une valeur, indiquant le statut de la tâche individuelle (3 tâches terminées sur 5, par exemple)

Demander une exécution pour un groupe de tâches et attendre son achèvement:

  • POST /tasks/groups avec le groupe de tâches à exécuter
    • GET /tasks/groups/group/[groupId]?awaitCompletion=true jusqu'à ce que répond avec un résultat qui indique l'achèvement (probablement a un délai d'attente, c'est pourquoi devrait être mis en boucle)
Ben Barkay
la source
Je pense que parler de ce qui a du sens sur le plan sémantique est la bonne façon d’aborder cela. Merci!
Anders
2
J'allais proposer cette réponse si elle n'y était pas déjà arrivée. Il est impossible de faire plusieurs demandes dans une seule demande HTTP. D'autre part, il est parfaitement possible de faire une seule requête HTTP qui dit "effectuez les actions suivantes et laissez-moi savoir quels sont les résultats". Et c'est ce qui se passe ici.
Martin Kochanski
J'accepterai cette réponse même si elle est loin du plus grand nombre de voix. Tandis que d'autres réponses sont bonnes aussi, je pense que c'est la seule qui raisonne sur la sémantique de HTTP.
Anders
87

Mon vote serait de scinder ces tâches en demandes séparées. Cependant, si trop d'allers et retours sont un sujet de préoccupation, j'ai rencontré le code de réponse HTTP 207 - Multi-Status

Copier / coller depuis ce lien:

Une réponse multi-états transmet des informations sur plusieurs ressources dans des situations où plusieurs codes d'état peuvent être appropriés. Le corps de la réponse Multi-Status par défaut est une entité HTTP text / xml ou application / xml avec un élément racine 'multistatus'. D'autres éléments contiennent les codes d'état des séries 200, 300, 400 et 500 générés lors de l'appel de la méthode. Les codes d'état de la série 100 NE DEVRAIENT PAS être enregistrés dans un élément XML 'réponse'.

Bien que "207" soit utilisé comme code de statut de réponse global, le destinataire doit consulter le contenu du corps de réponse multistatus pour obtenir des informations supplémentaires sur le succès ou l'échec de l'exécution du procédé. La réponse PEUT être utilisée en cas de succès, de succès partiel et aussi en cas d'échec.

Neilsimp1
la source
22
207semble être ce que veut le PO, mais je tiens vraiment à souligner que c’est probablement une mauvaise idée d’avoir cette approche à demandes multiples en un. Si le souci est la performance, alors vous devriez concevoir des systèmes évolutifs horizontaux de style cloud (ce qui rend les systèmes basés sur HTTP excellents)
Reinstate Monica
44
@ David Grinberg, je ne pouvais pas être plus en désaccord. Si les actions individuelles sont peu coûteuses, les frais généraux liés au traitement d'une demande peuvent être beaucoup plus coûteux que l'action elle-même. Votre suggestion pourrait conduire à des scénarios dans lesquels la mise à jour de plusieurs lignes dans une base de données est effectuée à l'aide d'une transaction distincte par ligne car chaque ligne est envoyée en tant que demande distincte. Ceci est non seulement terriblement inefficace, mais signifie également qu'il ne sera pas possible de mettre à jour plusieurs lignes de manière atomique si cela est nécessaire. La mise à l'échelle horizontale est importante, mais elle ne remplace pas les conceptions efficaces.
Kasperd
4
Bien dit et en soulignant un problème typique des implémentations d'API REST effectuées par des personnes ignorantes des réalités des besoins de l'entreprise, telles que les performances et / ou l'atomicité. C’est pourquoi, par exemple, la spécification OData REST a un format de traitement par lots pour les opérations multiples en un seul appel - il existe un réel besoin.
TomTom
8
@TomTom, le PO ne veut pas d'atomicité. Ce serait une chose beaucoup plus facile à concevoir, car il n'y a qu'un seul statut d'opération atomique. De plus, la spécification HTTP autorise les opérations de traitement par lots pour la performance, via le multiplexage HTTP / 2 (bien entendu, la prise en charge de HTTP / 2 est un autre problème, mais la spécification le permet).
Paul Draper
2
@David Ayant travaillé sur certains problèmes de calcul haute performance dans le passé, mon expérience m'a montré que le coût d'envoi d'un seul octet est quasiment identique à celui d'un millier (différents supports de transfert entraînent certes des frais généraux différents, mais rarement meilleurs que cela). Donc, si les performances sont une préoccupation, je ne vois pas en quoi l'envoi de plusieurs demandes n'aurait pas de frais supplémentaires importants. Maintenant, si vous pouviez multiplexer plusieurs demandes sur la même connexion, ce problème disparaîtrait, mais si j'ai bien compris, il ne s'agit que d'une option avec HTTP / 2 et sa prise en charge est plutôt limitée. Ou est-ce que je manque quelque chose?
Voo
24

Bien que le multi-statut soit une option, je retournerais 200 (tout va bien) si toutes les demandes aboutissent et une erreur (500 ou peut-être 207) sinon.

Le cas standard devrait normalement être 200 - tout fonctionne. Et les clients devraient seulement avoir à vérifier pour cela. Et ce n'est que si le cas d'erreur s'est produit que vous pouvez retourner un 500 (ou un 207). Je pense que le 207 est un choix valable dans le cas d'au moins une erreur, mais si vous voyez le paquet entier comme une transaction, vous pouvez aussi envoyer 500. - Le client voudra interpréter le message d'erreur dans un sens ou dans l'autre.

Pourquoi ne pas toujours envoyer 207? - Parce que les cas standards doivent être faciles et standard. Bien que des cas exceptionnels puissent être exceptionnels. Un client devrait seulement avoir à lire le corps de la réponse et à prendre d'autres décisions complexes, si une situation exceptionnelle le justifie.

Falco
la source
6
Je ne suis pas tout à fait d'accord. Si les sous-demandes 1 et 3 réussissent, vous obtenez une ressource combinée et devez quand même vérifier la réponse combinée. Vous avez juste un autre cas à considérer. Si réponse = 200 ou sous-réponse 1 = 200, la requête 1 a abouti. Si réponse = 200 ou sous-réponse 2 = 200, la requête 2 aboutit et ainsi de suite au lieu de simplement tester la sous-réponse.
gnasher729
1
@ gnasher729 cela dépend vraiment de l'application. J'imagine une action centrée sur l'utilisateur, qui passera simplement à l'étape suivante avec (tout va bien) lorsque toutes les demandes ont abouti. - Si quelque chose ne va pas (état global <= 200), vous devez alors afficher les erreurs détaillées et modifier le flux de travail. Un seul contrôle est nécessaire pour chaque sous-demande, car vous vous trouvez dans la fonction "handleMixedState" et non dans la fonction "handleAllOk". .
Falco
Cela dépend vraiment de ce que cela signifie. Par exemple, j'ai un terminal qui contrôle les stratégies de trading. Vous pouvez "démarrer" une liste d'identifiants en une fois. Retour 200 signifie que l'opération (les traiter) a réussi - mais tous ne peuvent pas démarrer correctement. Ce qui, d'ailleurs, ne peut même pas être vu dans le résultat immédiat (qui va commencer) car le démarrage peut prendre quelques secondes. La sémantique dans les appels multi-opération dépend du scénario.
TomTom
Je voudrais aussi probablement envoyer un 500 s'il y avait un problème général (par exemple, Base de données en panne) afin que le serveur n'essaye même pas les requêtes individuelles, mais puisse simplement renvoyer une erreur générale. - Parce qu'il y a 3 résultats différents pour l'utilisateur 1. Tout est OK, 2. Problème général, rien ne fonctionne, 3. Certaines demandes ont échoué. -> Ce qui entraînera généralement un flux de programme complètement différent.
Falco
1
Ok, une approche serait: 207 = statut individuel pour chaque demande. Autre chose: Le statut retourné s'applique à chaque demande. Ce qui est logique pour 200, 401, ≥ 500.
gnasher729
13

Une option serait de toujours renvoyer un code d'état 200, puis des erreurs spécifiques dans le corps du document JSON. C’est exactement comme cela que certaines API sont conçues (elles renvoient toujours un code d’état 200 et envoient l’erreur dans le corps). Pour plus de détails sur les différentes approches, voir http://archive.oreilly.com/pub/post/restful_error_handling.html.

BigONotation
la source
2
Dans ce cas, j'aime bien l'idée d'utiliser le 200pour indiquer que tout va bien, que la demande est reçue et était valide , puis que le JSON fournit des détails sur ce qui s'est passé dans cette demande (c'est-à-dire le résultat des transactions).
rickcnagy
4

Je pense que neilsimp1 est correct, mais je recommanderais de revoir la conception des données envoyées de manière à pouvoir les envoyer 206 - Acceptedet les traiter ultérieurement. Peut-être avec des rappels.

Le problème lié à l’envoi de plusieurs actions dans une même demande est précisément le fait que chaque action doit avoir son propre "statut"

En regardant importer un CSV (je ne sais pas vraiment ce qu'est le PO mais c'est une version simple). POST le fichier CSV et récupère un 206. Ensuite, le fichier CSV peut être importé et vous pouvez obtenir le statut de l'importation avec un élément GET (200) par rapport à une URL indiquant les erreurs par ligne.

POST /imports/ -> 206
GET  /imports/1 -> 200
GET  /imports/1/errors -> 200 -> Has a list of errors

Ce même motif peut être appliqué à de nombreuses opérations par lots

POST /operations/ -> 206
GET  /operations/1 -> 200
GET  /operations/1/errors -> 200 - > Has a list of errors.

Le code qui gère le POST n'a qu'à vérifier que le format des données d'opération est valide. Ensuite, les opérations peuvent être exécutées ultérieurement. Dans un travailleur de fond, vous pouvez ainsi évoluer plus facilement, par exemple. Ensuite, vous pouvez vérifier l’état des opérations à tout moment. Vous pouvez utiliser l'interrogation ou des rappels, des flux ou autres, pour répondre au besoin de savoir quand un ensemble d'opérations est terminé.

coteyr
la source
2

Déjà beaucoup de bonnes réponses ici, mais il manque un aspect:

Quel est le contrat que vos clients attendent?

Les codes de retour HTTP doivent indiquer au moins une distinction entre succès et échec et jouer ainsi le rôle d '"exceptions du pauvre". Ensuite, 200 signifie "contrat entièrement rempli", et 4xx ou 5xx indiquent un manquement.

Naïvement, je m'attendrais à ce que le contrat de votre demande d'actions multiples soit "effectue toutes mes tâches", et si l'une d'entre elles échoue, la demande n'aboutit pas (complètement). En général, en tant que client, je comprendrais 200 comme "tout va bien", et les codes des familles 400 et 500 me forcent à réfléchir aux conséquences d'un échec (partiel). Utilisez donc 200 pour "toutes les tâches effectuées" et 500 plus une réponse descriptive en cas d’échec partiel.

Un contrat hypothétique différent pourrait être "essayez de faire toutes les actions". Ensuite, le contrat est complètement conforme si (certaines) des actions échouent. Ainsi, vous retournerez toujours 200 plus un document de résultats dans lequel vous trouverez les informations de réussite / échec pour les tâches individuelles.

Alors, quel est le contrat que vous voulez suivre? Les deux sont valables, mais le premier (200 seulement si tout a été fait) est plus intuitif pour moi et mieux adapté aux modèles logiciels habituels. Et dans la majorité (espérons-le) cas où le service a exécuté toutes les tâches, il est facile pour le client de détecter ce cas.

Un dernier aspect important: comment communiquez-vous la décision de votre contrat à vos clients? Par exemple, en Java, j'utiliserais des noms de méthodes tels que "doAll ()" ou "tryToDoAll ()". Dans HTTP, vous pouvez nommer les URL de point de terminaison en conséquence, en espérant que les développeurs de votre client voient, lisent et comprennent le nommage (je ne parierais pas là-dessus). Une raison de plus pour choisir le contrat de moindre surprise.

Ralf Kleberhoff
la source
0

Répondre:

Utilisez juste une requête par action et acceptez les frais supplémentaires.

Un code d'état décrit le statut d'une opération. Il est donc logique d’avoir une opération par requête.

Plusieurs opérations indépendantes détruisent le principal sur lequel sont basés le modèle de requête-réponse et les codes d'état. Tu combats la nature.

HTTP / 1.1 et HTTP / 2 ont considérablement réduit le temps système lié aux requêtes HTTP. J’estime qu’il est recommandé de regrouper des demandes indépendantes par lot.


Cela dit,

(1) Vous pouvez effectuer plusieurs modifications avec une requête PATCH ( RFC 5789 ). Cependant, cela nécessite que les modifications ne soient pas indépendantes; ils sont appliqués de manière atomique (tout ou rien).

(2) D'autres ont signalé le code 207 Multi-Status. Toutefois, cela n'est défini que pour WebDAV ( RFC 4918 ), une extension de HTTP.

Le code d’état 207 (multi-états) indique l’état de plusieurs opérations indépendantes (voir la section 13 pour plus d’informations).

...

Une réponse multi-états transmet des informations sur plusieurs ressources dans des situations où plusieurs codes d'état peuvent être appropriés. L'élément racine [XML] 'multistatus' contient zéro ou plusieurs éléments 'réponse' dans n'importe quel ordre, chacun contenant des informations sur une ressource individuelle.

Une réponse 207 WebDAV XML serait aussi étrange qu'un canard dans une API non WebDAV. Ne fais pas ça.

Paul Draper
la source
1
Vous affirmez essentiellement que @Anders a un problème XY . Vous avez peut-être raison, mais malheureusement, cela signifie que vous n'avez pas répondu à la question qu'il a posée (quel code d'état utiliser pour une demande à actions multiples).
Azuaron
2
@ Azuaron, quel genre de ceinture fonctionne le mieux pour battre les enfants? Je pense que "N / A" est une réponse admissible. De plus, Andres a inclus plusieurs demandes dans sa liste d'idées. J'ai appuyé sans réserve cette option.
Paul Draper
J'ai raté qu'il ait énuméré cela. Dans ce cas, j’affirme que c’est une question idiote, Votre Honneur!
Azuaron
1
@ Azuaron Je pense absolument que cette réponse est valable. Si je fais tout faux, je veux que quelqu'un le dise, et ne me donne pas d'instructions sur la meilleure façon de chasser une falaise.
Anders
1
Rien n'interdit d'envoyer du JSON dans la réponse 207, tant que l'en-tête Content-Type est correctement défini et correspond à ce que le client a demandé (en-tête Accept).
dolmen
0

Si vous avez vraiment besoin de plusieurs actions dans une requête, pourquoi ne pas envelopper toutes les actions d'une transaction dans le backend? De cette façon, ils réussissent ou échouent tous.

En tant que client utilisant l'API, je peux gérer le succès ou l'échec complet d'un appel d'API. Le succès partiel est difficile à gérer, car je devrais gérer tous les états possibles.

doyen
la source
2
Je suppose que si la demande était atomique, il n'aurait pas posté cette question.
Andy
@Andy Peut-être, mais vous ne pouvez pas supposer qu'il a pris en compte toutes les implications d'une telle conception.
Dean
La requête ne doit pas être atomique - par exemple, si # 2 échoue, les modifications apportées par # 1 doivent toujours être conservées. Donc, tout emballer dans une transaction n'est pas une option.
Anders