Je suis intéressé à exposer une interface REST directe à des collections de documents JSON (pensez CouchDB ou Persevere ). Le problème que je rencontre est de savoir comment gérer l' GET
opération sur la racine de la collection si la collection est grande.
À titre d'exemple, prétendez que j'expose la Questions
table de StackOverflow où chaque ligne est exposée en tant que document (non pas qu'il existe nécessairement une telle table, juste un exemple concret d'une collection importante de «documents»). La collection sera mis à disposition à /db/questions
la api habituelle CRUD GET /db/questions/XXX
, PUT /db/questions/XXX
, POST /db/questions
est en jeu. Le moyen standard d'obtenir toute la collection est de le faire, GET /db/questions
mais si cela vide naïvement chaque ligne en tant qu'objet JSON, vous obtiendrez un téléchargement assez important et beaucoup de travail de la part du serveur.
La solution est, bien sûr, la pagination. Dojo a résolu ce problème dans son JsonRestStore via une extension intelligente conforme à la RFC2616 d'utilisation de l'en- Range
tête avec une unité de plage personnalisée items
. Le résultat est un 206 Partial Content
qui renvoie uniquement la plage demandée. L'avantage de cette approche par rapport à un paramètre de requête est qu'elle laisse la chaîne de requête pour ... requêtes (par exemple GET /db/questions/?score>200
ou une partie, et oui qui serait encodée %3E
).
Cette approche couvre complètement le comportement que je souhaite. Le problème est que la RFC 2616 spécifie que sur une réponse 206 (c'est moi qui souligne):
La demande DOIT avoir inclus un champ d'en-tête Range ( section 14.35 ) indiquant la gamme souhaitée, et PEUT avoir inclus un champ d'en-tête If-Range ( section 14.27 ) pour rendre la demande conditionnelle.
Cela a du sens dans le contexte de l'utilisation standard de l'en-tête, mais c'est un problème car j'aimerais que la réponse 206 soit la réponse par défaut pour gérer les clients naïfs / les personnes aléatoires explorant.
J'ai parcouru la RFC en détail à la recherche d'une solution, mais je n'ai pas été satisfait de mes solutions et je suis intéressé par la prise en charge du problème par les SO.
Idées que j'ai eues:
- Revenez
200
avec un en-Content-Range
tête! - Je ne pense pas que ce soit faux, mais je préférerais un indicateur plus évident que la réponse n'est qu'un contenu partiel. - Retour
400 Range Required
- Il n'y a pas de code de réponse 400 spécial pour les en-têtes requis, donc l'erreur par défaut doit être utilisée et lue à la main. Cela rend également l'exploration via un navigateur Web (ou un autre client comme Resty) plus difficile. - Utilisez un paramètre de requête - L'approche standard, mais j'espère autoriser les requêtes à la persévérance et cela coupe dans l'espace de noms de la requête.
- Revenez juste
206
! - Je pense que la plupart des clients ne paniqueraient pas, mais je préfère ne pas aller à l'encontre d'un MUST dans la RFC - Prolongez les spécifications! Retour
266 Partial Content
- Se comporte exactement comme 206 mais est en réponse à une demande qui NE DOIT PAS contenir l'en-Range
tête. Je pense que 266 est suffisamment élevé pour que je ne devrais pas rencontrer de problèmes de collision et cela a du sens pour moi, mais je ne sais pas si cela est considéré comme tabou ou non.
Je pense que c'est un problème assez courant et j'aimerais que cela se fasse d'une manière de facto afin que moi ou quelqu'un d'autre ne réinvente pas la roue.
Quelle est la meilleure façon d'exposer une collection complète via HTTP lorsque la collection est volumineuse?
la source
Range = "Range" ":" ranges-specifier
où ce dernier dans tools.ietf.org/html/rfc2616#section-14.35.1 est simplement décrit comme "byte-range-specifier" qui doit commencer par "bytes-unit" qui est défini comme la chaîne "bytes ".Content-Range
tête s'applique au corps (peut être utilisé avec une requête lors du téléchargement de gros fichiers, etc., ou pour une réponse lors du téléchargement). L'en-Range
tête est utilisé pour demander une certaine plage. Il faut répondre206
quand l'en-Range
tête a été inclus dans la demande. Si ce n'est pas le cas, la réponse peut toujours inclure un en-Content-Range
tête, mais le code de réponse doit l'être200
. Cet en-tête semble en fait idéal pour la pagination.Réponses:
Mon instinct est que les extensions de plage HTTP ne sont pas conçues pour votre cas d'utilisation et que vous ne devriez donc pas essayer. Une réponse partielle implique
206
et206
ne doit être envoyée que si le client l'a demandé.Vous voudrez peut-être envisager une approche différente, telle que celle utilisée dans Atom (où la représentation par conception peut être partielle et est renvoyée avec un statut
200
et potentiellement des liens de pagination). Voir RFC 4287 et RFC 5005 .la source
items
unité de plage, il renvoie une réponse complète. Je connais Atom mais ce n'est pas la solution générale à la pagination Rest. Ce n'est pas une solution pour un seul cas, mais plutôt ce que devrait être la solution générale. Tous les documents / collections ne correspondent pas au modèle Atom et il n'y a aucune raison de le forcer sauf si nécessaire.Range
etContent-Range
à des fins de pagination.Je ne suis pas vraiment d'accord avec certains d'entre vous. Je travaille depuis des semaines sur ces fonctionnalités pour mon service REST. Ce que j'ai fini par faire est vraiment simple. Ma solution n'a de sens que pour ce que les gens de REST appellent une collection.
Le client DOIT inclure un en-tête "Range" pour indiquer la partie de la collection dont il a besoin, ou être prêt à gérer une erreur 413 REQUESTED ENTITY TOO LARGE lorsque la collection demandée est trop grande pour être récupérée en un seul aller-retour.
Le serveur envoie une réponse 206 PARTIAL CONTENT, avec l'en-tête Content-Range spécifiant quelle partie de la ressource a été envoyée, et un en-tête ETag pour identifier la version actuelle de la collection. J'utilise généralement un ETag {last_modification_timestamp} - {resource_id} de type Facebook, et je considère que l'ETag d'une collection est celui de la ressource la plus récemment modifiée qu'elle contient.
Pour demander une partie spécifique d'une collection, le client DOIT utiliser l'en-tête "Range", et remplir l'en-tête "If-Match" avec l'ETag de la collection obtenue à partir de demandes précédemment effectuées pour acquérir d'autres parties de la même collection. Le serveur peut donc vérifier que la collection n'a pas changé avant d'envoyer la partie demandée. Si une version plus récente existe, une réponse 412 PRECONDITION FAILED est renvoyée pour inviter le client à récupérer la collection à partir de zéro. Cela est nécessaire car cela peut signifier que certaines ressources peuvent avoir été ajoutées ou supprimées avant ou après la partie actuellement demandée.
J'utilise ETag / If-Match en tandem avec Last-Modified / If-Unmodified-Since pour optimiser le cache. Les navigateurs et les mandataires peuvent s'appuyer sur l'un d'entre eux ou sur les deux pour leurs algorithmes de mise en cache.
Je pense qu'une URL devrait être propre à moins qu'elle n'inclue une requête de recherche / filtre. Si vous y réfléchissez, une recherche n'est rien de plus qu'une vue partielle d'une collection. Au lieu des URL de type voitures / recherche? Q = BMW, nous devrions voir plus de voitures? Fabricant = BMW.
la source
If-Unmodified-Since
, ce qui correspond à la variante E-TagIf-Match
, plutôt queIf-Modified-Since
. Cela dit, vous pouvez également envisager de supprimer cette contrainte, en fonction de votre cas d'utilisation. Supposons que vous ayez une collection qui ne grandit qu'à partir du haut (comme certaines collections de style "la plus récente en premier"), le pire qui puisse arriver si cette collection change entre les demandes est qu'un utilisateur qui parcourt une collection voit les entrées deux fois. (Ce qui en soi est également une information utile: il indique à l'utilisateur que la collection a changé)413
. Il s'agit d'un code d'erreur signifiant que le client envoie quelque chose que le serveur refuse d'accepter en raison de sa taille. Pas l'inverse! Voir tools.ietf.org/html/rfc7231#section-6.5.11 (notez que cela dit charge utile de demande . Pas de charge utile de réponse )!Vous pouvez toujours revenir
Accept-Ranges
etContent-Ranges
avec un200
code de réponse. Ces deux en-têtes de réponse vous donnent suffisamment d'informations pour déduire les mêmes informations qu'un206
code de réponse fournit explicitement.J'utiliserais
Range
pour la pagination et lui demanderais simplement de renvoyer un200
pour une plaineGET
.Cela semble 100% REST et ne rend pas la navigation plus difficile.
Edit: j'ai écrit un article de blog à ce sujet: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html
la source
S'il y a plus d'une page de réponses et que vous ne souhaitez pas proposer l'ensemble de la collection en même temps, cela signifie-t-il qu'il y a plusieurs choix?
Sur une demande à
/db/questions
, retournez300 Multiple Choices
avec desLink
en-têtes qui spécifient comment accéder à chaque page ainsi qu'un objet JSON ou une page HTML avec une liste d'URL.Vous auriez un en-
Link
tête pour chaque page de résultats (une chaîne vide signifie l'URL actuelle, et l'URL est la même pour chaque page, juste accessible avec différentes plages), et la relation est définie comme uneLink
relation personnalisée selon la spécification à venir . Cette relation expliquerait votre coutume266
ou votre violation de206
. Ces en-têtes sont votre version lisible par machine, car tous vos exemples nécessitent de toute façon un client compréhensif.(Si vous vous en tenez à la route "range", je crois que votre propre
2xx
code de retour, tel que vous l'avez décrit, serait le meilleur comportement ici. Vous êtes censé le faire pour vos applications et ces ["codes d'état HTTP sont extensibles. "], et vous avez de bonnes raisons.)300 Multiple Choices
dit que vous DEVRIEZ également fournir à un organisme un moyen pour l'agent utilisateur de choisir. Si votre client est compréhensif, il doit utiliser les en-Link
têtes. Si c'est un utilisateur qui navigue manuellement, peut-être une page HTML avec des liens vers une ressource racine spéciale "paginée" qui peut gérer le rendu de cette page particulière en fonction de l'URL?/humanpage/1/db/questions
ou quelque chose de hideux comme ça?Les commentaires sur le post de Richard Levasseur me rappellent une option supplémentaire: l'en-
Accept
tête (section 14.1). À l'époque où la spécification oEmbed est sortie, je me suis demandé pourquoi cela n'avait pas été entièrement fait en utilisant HTTP, et j'ai écrit une alternative en les utilisant.Gardez le
300 Multiple Choices
, les en-Link
têtes et la page HTML pour un HTTP naïf initialGET
, mais plutôt que d'utiliser des plages, faites en sorte que votre nouvelle relation de pagination définisse l'utilisation de l'en-Accept
tête. Votre requête HTTP suivante pourrait ressembler à ceci:L'en-
Accept
tête vous permet de définir un type de contenu acceptable (votre retour JSON), ainsi que des paramètres extensibles pour ce type (votre numéro de page). En reprenant mes notes de mon écriture oEmbed (je ne peux pas le lier ici, je vais le lister dans mon profil), vous pourriez être très explicite et fournir une version de spécification / relation ici au cas où vous auriez besoin de redéfinir ce quepage
signifie le paramètre A l'avenir.la source
Éditer:
Après y avoir réfléchi un peu plus, je suis enclin à convenir que les en-têtes Range ne sont pas appropriés pour la pagination. La logique étant, l'en-tête Range est destiné à la réponse du serveur, pas aux applications. Si vous avez fourni 100 mégaoctets de résultats, mais que le serveur (ou client) ne pouvait traiter que 1 mégaoctet à la fois, eh bien, c'est à cela que sert l'en-tête Range.
Je suis également d'avis qu'un sous-ensemble de ressources est sa propre ressource (similaire à l'algèbre relationnelle), donc il mérite d'être représenté dans l'URL.
Donc, fondamentalement, je renonce à ma réponse originale (ci-dessous) sur l'utilisation d'un en-tête.
Je pense que vous avez répondu à votre propre question, plus ou moins - renvoyez 200 ou 206 avec content-range et utilisez éventuellement un paramètre de requête. Je reniflerais l'agent utilisateur et le type de contenu et, en fonction de ceux-ci, vérifierais un paramètre de requête. Sinon, exigez les en-têtes de plage.
Vous avez essentiellement des objectifs contradictoires - laissez les gens utiliser leur navigateur pour explorer (ce qui n'autorise pas facilement les en-têtes personnalisés), ou forcez les gens à utiliser un client spécial qui peut définir des en-têtes (ce qui ne les laisse pas explorer).
Vous pouvez simplement leur fournir le client spécial en fonction de la demande - s'il ressemble à un navigateur ordinaire, envoyez une petite application ajax qui rend la page et définit les en-têtes nécessaires.
Bien sûr, il y a aussi le débat sur la question de savoir si l'URL doit contenir tout l'état nécessaire pour ce genre de chose. Spécifier la plage en utilisant des en-têtes peut être considéré comme "non reposant" par certains.
En passant, ce serait bien si les serveurs pouvaient répondre avec un en-tête «Can-Specify: Header1, header2» et que les navigateurs Web présentent une interface utilisateur afin que les utilisateurs puissent remplir des valeurs, s'ils le souhaitent.
la source
Vous pouvez envisager d'utiliser un modèle comme le Atom Feed Protocol car il a un modèle HTTP sain de collections et comment les manipuler (où insane signifie WebDAV).
Il existe le protocole de publication Atom qui définit le modèle de collection et les opérations REST, ainsi que la RFC 5005 - Feed Paging and Archiving pour parcourir de grandes collections.
Le passage du contenu Atom XML au contenu JSON ne devrait pas affecter l'idée.
la source
Je pense que le vrai problème ici est qu'il n'y a rien dans la spécification qui nous indique comment faire des redirections automatiques face à 413 - Requested Entity Too Large.
J'étais aux prises avec ce même problème récemment et j'ai cherché l'inspiration dans le livre RESTful Web Services . Personnellement, je ne pense pas que 206 soit approprié en raison de l'exigence d'en-tête. Mes pensées m'ont également conduit à 300, mais je pensais que c'était plus pour différents types de mime, alors j'ai regardé ce que Richardson et Ruby avaient à dire sur le sujet dans l'annexe B, page 377. Ils suggèrent que le serveur choisisse simplement le serveur préféré. représentation et renvoyez-le avec un 200, en ignorant fondamentalement la notion que ce devrait être un 300.
Cela concorde également avec la notion de liens vers les prochaines ressources que nous avons d'atome. La solution que j'ai implémentée était d'ajouter les clés «suivant» et «précédent» à la carte json que je renvoyais et en finir avec elle.
Plus tard, j'ai commencé à penser que la chose à faire est peut-être d'envoyer un 307 - Redirection temporaire vers un lien qui serait quelque chose comme / db / questions / 1,25 - qui laisse l'URI d'origine comme nom canonique de la ressource, mais cela vous amène à une ressource subordonnée correctement nommée. C'est un comportement que j'aimerais voir sur un 413, mais le 307 me semble un bon compromis. Cependant, je n'ai pas encore essayé cela dans le code. Ce qui serait encore mieux, c'est que la redirection soit redirigée vers une URL contenant les identifiants réels des questions les plus récemment posées. Par exemple, si chaque question a un identifiant entier, et qu'il y a 100 questions dans le système et que vous voulez afficher les dix plus récentes, les demandes à / db / questions doivent être 307 à / db / questions / 100,91
C'est une très bonne question, merci de l'avoir posée. Vous m'avez confirmé que je ne suis pas fou d'avoir passé des jours à y penser.
la source
Vous pouvez détecter l'en-
Range
tête et imiter Dojo s'il est présent, et imiter Atom si ce n'est pas le cas. Il me semble que cela divise parfaitement les cas d'utilisation. Si vous répondez à une requête REST de votre application, vous vous attendez à ce qu'elle soit mise en forme avec un en-Range
tête. Si vous répondez à un navigateur occasionnel, si vous renvoyez des liens de pagination, l'outil vous permettra d'explorer facilement la collection.la source
L'un des gros problèmes des en-têtes de plage est que de nombreux proxys d'entreprise les filtrent. Je conseillerais d'utiliser un paramètre de requête à la place.
la source
Avec la publication de rfc723x , les unités de gamme non enregistrées vont à l'encontre d'une recommandation explicite dans la spécification . Considérez rfc7233 (obsolète rfc2616):
«Les nouvelles unités de plage doivent être enregistrées auprès de l'IANA » (avec une référence à un registre d'unités de plage HTTP ).
la source
Il me semble que la meilleure façon de procéder consiste à inclure la plage en tant que paramètres de requête. par exemple, GET / db / questions /? date> mindate & date <maxdate . Lors d'un GET vers / db / questions / sans paramètres de requête, renvoyez 303 avec Location: / db / questions /? Query-parameters-to-retrieve-the-default-page . Ensuite, fournissez une URL différente par laquelle quiconque consomme votre API pour obtenir des statistiques sur la collection (par exemple, quels paramètres de requête utiliser s'il / elle veut la collection entière);
la source
Bien qu'il soit possible d'utiliser l'en-tête Range à cette fin, je ne pense pas que c'était l'intention. Il semble avoir été conçu pour gérer les connexions irrégulières ainsi que pour limiter les données (afin que le client puisse demander une partie de la requête si quelque chose manquait ou si la taille était trop grande pour être traitée). Vous piratez la pagination dans quelque chose qui est probablement utilisé à d'autres fins au niveau de la couche de communication. La "bonne" façon de gérer la pagination est d'utiliser les types que vous renvoyez. Plutôt que de renvoyer l'objet de questions, vous devriez renvoyer un nouveau type à la place.
Donc, si les questions sont comme ça:
<questions> <question index=1></question> <question index=2></question> ... </questions>
Le nouveau type pourrait être quelque chose comme ceci:
<questionPage> <startIndex>50</startIndex> <returnedCount>10</returnedCount> <totalCount>1203</totalCount> <questions> <question index=50></question> <question index=51></question> .. </questions> <questionPage>
Bien sûr, vous contrôlez vos types de médias, vous pouvez donc faire de vos "pages" un format qui répond à vos besoins. Si vous créez quelque chose de générique, vous pouvez avoir un seul analyseur sur le client pour gérer la pagination de la même manière pour tous les types. Je pense que c'est plus dans l'esprit de la spécification HTTP, plutôt que de truquer le paramètre Range pour autre chose.
la source