Je souhaite exposer une interface directe REST à des collections de documents JSON (think CouchDB ou Persevere ). Le problème que je rencontre est de savoir comment gérer l'opération GET
sur la racine de la collection si la collection est volumineuse.
Par exemple, supposons que j'expose la table Questions
de StackOverflow dans laquelle chaque ligne est exposée en tant que document (pas qu'il existe nécessairement une telle table, mais juste un exemple concret d'une importante collection de 'documents'). La collection serait mise à disposition au /db/questions
avec l’appel CRUD habituel GET /db/questions/XXX
, PUT /db/questions/XXX
, POST /db/questions
en cours de lecture. La méthode standard pour obtenir la totalité de la collection consiste à GET /db/questions
, mais si cela vide naïvement chaque ligne en tant qu’objet JSON, vous aurez un téléchargement assez volumineux 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 astucieuse conforme à la norme RFC2616 consistant à utiliser l'en-tête Range
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’il laisse la chaîne de requête pour ... requêtes (par exemple, GET /db/questions/?score>200
ou une telle méthode, et oui, elle serait codée %3E
).
Cette approche couvre complètement le comportement que je veux. Le problème est que RFC 2616 précise que sur une réponse 206 (mon accentuation):
La demande DOIT avoir inclus un champ d'en-tête Range ( section 14.35 ) indiquant la plage souhaitée, et PEUT avoir inclus une gamme If champ d'en-tête ( 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 je souhaiterais que la réponse 206 soit la réponse par défaut à la gestion de clients naïfs/de personnes aléatoires.
J'ai examiné la RFC en détail à la recherche d'une solution, mais je suis mécontent de mes solutions et je suis intéressé par la résolution du problème.
Idées que j'ai eues:
200
avec un en-tête Content-Range
! _ Je ne pense pas que cela soit faux, mais je préférerais un indicateur plus évident que la réponse est seulement un contenu partiel.400 Range Required
- Il n'y a pas de code de réponse spécial 400 pour les en-têtes requis. L'erreur par défaut doit donc être utilisée et lue à la main. Cela rend également l'exploration via un navigateur Web (ou un autre client tel que Resty) plus difficile.206
! - Je pense que la plupart des clients ne paniqueraient pas, mais je préférerais ne pas aller contre un MUST dans le RFC.266 Partial Content
- Se comporte exactement comme 206 mais répond à une demande qui NE DOIT PAS contenir l'en-tête Range
. Je pense que 266 est suffisamment élevé pour ne pas me heurter à des problèmes de collision. Cela me semble logique, mais je ne vois pas très bien 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 de manière factuelle afin que moi-même ou quelqu'un d'autre ne réinventions pas la roue.
Quel est le meilleur moyen d'exposer une collection complète via HTTP lorsque la collection est volumineuse?
Mon sentiment est que les extensions de la 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
et 206
ne doit être envoyé que si le client le demande.
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 renvoyée avec un statut 200
et éventuellement des liens de pagination). Voir RFC 4287 et RFC 5005 .
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 REST appelle 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 à traiter une erreur 413 REQUISE ENTITY TOO LARGE lorsque la collection demandée est trop volumineuse pour pouvoir être extraite en un seul aller-retour.
Le serveur envoie une réponse 206 PARTIAL CONTENT, avec l'en-tête Content-Range spécifiant la partie de la ressource qui 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}} semblable à Facebook, et je considère que l'ETag d'une collection est celui de la dernière ressource modifiée qu'elle contient.
Pour demander une partie spécifique d'une collection, le client DOIT utiliser l'en-tête "Range" et renseigner l'en-tête "If-Match" avec l'ETag de la collection obtenue à partir de demandes précédemment exécuté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 ont peut-être é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 ou les deux pour leurs algorithmes de mise en cache.
Je pense qu'une URL devrait être propre à moins d'inclure une requête de recherche/filtrage. Si vous y réfléchissez, une recherche n’est rien de plus qu’une vue partielle d’une collection. Au lieu des voitures/recherche? Q = type d'URL BMW, nous devrions voir plus de voitures? Constructeur = BMW.
S'il y a plus d'une page de réponses et que vous ne voulez pas offrir la collection complète en même temps, cela signifie-t-il qu'il y a plusieurs choix?
Sur une demande adressée à /db/questions
, renvoyez 300 Multiple Choices
avec les en-têtes Link
qui spécifient comment accéder à chaque page, ainsi qu'un objet JSON ou une page HTML avec une liste d'URL.
Link: <>; rel="http://paged.collection.example/relation/paged"
Link: <>; rel="http://paged.collection.example/relation/paged"
...
Vous auriez un en-tête Link
pour chaque page de résultats (une chaîne vide indique l'URL actuelle, et l'URL est la même pour chaque page, accessible uniquement avec des plages différentes), et la relation est définie comme une valeur personnalisée par la prochaine spécification Link
name__ . Cette relation expliquerait votre 266
personnalisé ou votre violation de 206
. 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 "plage", je pense que votre propre code de retour 2xx
, tel que vous l'avez décrit, serait le meilleur comportement à adopter ici. Vous êtes censé le faire pour vos applications et autres ["HTTP les codes de statut sont extensibles. "], et vous avez de bonnes raisons.)
300 Multiple Choices
indique que vous DEVEZ également fournir un corps avec un moyen de sélection pour l'agent utilisateur. Si votre client comprend, il doit utiliser les en-têtes Link
name__. Si c'est un utilisateur qui navigue manuellement, peut-être une page HTML avec des liens vers une ressource racine spéciale "paginée" pouvant gérer le rendu de cette page particulière en fonction de l'URL? /humanpage/1/db/questions
ou quelque chose d'aussi hideux?
Les commentaires sur le message de Richard Levasseur me font penser à une option supplémentaire: l'en-tête Accept
(section 14.1). À l'époque de la publication de la spécification oEmbed, je me suis demandé pourquoi cela n'avait pas été fait entièrement avec HTTP et j'ai écrit une alternative pour les utiliser.
Conservez les en-têtes 300 Multiple Choices
, Link
et la page HTML pour un nom HTTP naïf GET
____ naïf, mais plutôt que des plages d'utilisation, définissez l'utilisation de l'en-tête Accept
name__. Votre requête HTTP ultérieure pourrait ressembler à ceci:
GET /db/questions HTTP/1.1
Host: paged.collection.example
Accept: application/json;PagingSpec=1.0;page=1
L'en-tête Accept
vous permet de définir un type de contenu acceptable (votre déclaration JSON), ainsi que des paramètres extensibles pour ce type (votre numéro de page). En reprenant mes notes dans mon article oEmbed (je ne peux pas le lier ici, je le ferai dans mon profil), vous pouvez être très explicite et fournir une version spéc/relation ici au cas où vous auriez besoin de redéfinir le paramètre page
signifie à l'avenir.
Vous pouvez toujours renvoyer Accept-Ranges
et Content-Ranges
avec un code de réponse 200
. Ces deux en-têtes de réponse vous fournissent suffisamment d'informations pour infer les mêmes informations qu'un code de réponse 206
fournit explicitement.
J'utiliserais Range
pour la pagination et le renverrais simplement un 200
pour un GET
simple.
Cela donne 100% de repos et ne rend pas la navigation plus difficile.
Edit: J'ai écrit un billet de blog à ce sujet: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html
Je pense que le vrai problème ici est que rien dans la spécification ne nous dit comment faire des redirections automatiques face à 413 - Entité demandée trop grande.
Je me débattais avec le même problème récemment et je cherchais de 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 amené à 300, mais je pensais que c'était plus pour différents types de mimes, alors j'ai regardé ce que Richardson et Ruby avaient à dire à ce sujet à l'Annexe B, page 377. Ils suggèrent que le serveur choisisse simplement le fichier préféré. représentation et le renvoyer avec un 200, en ignorant essentiellement la notion qu'il devrait être un 300.
Cela concorde également avec la notion de liens vers les ressources suivantes que nous avons d’atome. La solution que j’ai mise en œuvre consistait à ajouter les clés "next" et "previous" à la carte json que je renvoyais et d’en finir.
Plus tard, j'ai commencé à penser que la chose à faire était peut-être d'envoyer 307 - Redirections temporaires vers un lien qui ressemblerait à quelque chose du genre/db/questions/1,25 - qui laisse l'URI d'origine en tant que nom de ressource canonique, mais cela vous amène à une ressource subordonnée nommée de manière appropriée. C'est un comportement que j'aimerais voir sur un 413, mais le 307 semble un bon compromis. Je n'ai pas encore réellement essayé cela dans le code. Ce qui serait encore mieux, c’est que la redirection redirige vers une URL contenant les identifiants réels des dernières questions posées. Par exemple, si chaque question a un ID entier et qu'il y a 100 questions dans le système et que vous souhaitez afficher les dix plus récentes, les demandes adressées à/db/questions doivent être traitées dans/db/questions/100,91.
C'est une très bonne question, merci de le poser. Vous m'avez confirmé que je ne suis pas cinglé pour avoir passé des jours à y penser.
Modifier:
Après y avoir réfléchi un peu plus, je suis enclin à convenir que les en-têtes Range ne conviennent pas à la pagination. La logique étant, l'en-tête Range est destiné à la réponse du serveur, pas aux applications. Si vous avez servi 100 mégaoctets de résultats, mais que le serveur (ou le client) ne peut traiter qu'un mégaoctet à la fois, eh bien, c'est à quoi 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). Elle mérite donc d'être représentée dans l'URL.
Donc, fondamentalement, je renie ma réponse originale (ci-dessous) sur l’utilisation d’un en-tête.
Je pense que vous avez plus ou moins répondu à votre propre question - renvoyez 200 ou 206 avec content-range et utilisez éventuellement un paramètre de requête. Je voudrais renifler l'agent utilisateur et le type de contenu et, en fonction de ceux-ci, rechercher un paramètre de requête. Sinon, utilisez les en-têtes de plage.
Vous avez essentiellement des objectifs contradictoires: permettre aux utilisateurs d’utiliser leur navigateur pour explorer (ce qui ne permet pas facilement des en-têtes personnalisés) ou les forcer à utiliser un client spécial pouvant 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 simple navigateur, envoyez une petite application ajax qui restitue la page et définit les en-têtes nécessaires.
Bien sûr, il y a aussi le débat sur le fait de savoir si l'URL doit contenir tout l'état nécessaire pour ce genre de chose. La spécification de la plage à l'aide d'en-têtes peut être considérée comme "non reposante" par certains.
Soit dit en passant, il serait agréable que les serveurs répondent avec un en-tête "Spécification canable: en-tête1, en-tête2" et que les navigateurs Web présentent une interface utilisateur permettant aux utilisateurs de saisir des valeurs s'ils le souhaitent.
Vous pouvez envisager d'utiliser un modèle similaire au protocole de flux Atom, car il possède un modèle HTTP sain de collections et explique comment les manipuler (où alias WebDAV).
Il existe le Atom Publishing Protocol qui définit le modèle de collection et les opérations REST. Vous pouvez également utiliser RFC 5005 - Pagination et archivage de flux pour parcourir de grandes collections.
Passer du contenu Atom XML au contenu JSON ne devrait pas affecter l'idée.
Avec la publication de rfc723x , unités de plage non enregistrées vont à l’encontre d’une recommandation explicite dans la spécification. Considérez rfc7233 (dépréciant rfc2616):
" Les nouvelles unités de gamme doivent être enregistrées auprès de IANA " (avec une référence à un HTTP Range Unit Registry ).
Vous pouvez détecter l'en-tête Range
et imiter Dojo s'il est présent et imiter Atom si ce n'est pas le cas. Il me semble que cela divise nettement les cas d'utilisation. Si vous répondez à une requête REST à partir de votre application, vous vous attendez à ce qu'elle soit formatée avec un en-tête Range
. Si vous répondez à un navigateur occasionnel, si vous renvoyez des liens de pagination, l'outil sera un moyen simple d'explorer la collection.
L'un des gros problèmes des en-têtes de plage est que beaucoup de mandataires d'entreprise les filtrent. Je conseillerais d'utiliser un paramètre de requête à la place.
Il me semble que la meilleure façon de faire est d’inclure une plage comme paramètre de requête. Par exemple, GET/db/questions /? date> état & date <date max. . Sur un objet GET dans/db/questions/sans paramètre de requête, retournez 303 avec Emplacement:/db/questions /? Query-parameters-to-extraire-la-page-par-défaut . Indiquez ensuite une autre URL, en fonction de la consommation de votre API, pour obtenir des statistiques sur la collection (par exemple, quels paramètres de requête utiliser si il/elle souhaite disposer de la totalité de la collection);
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 floconneuses et limiter les données (afin que le client puisse demander une partie de la demande si quelque chose manquait ou si la taille était trop grande pour être traitée). Vous piratez la pagination en un objet susceptible d’être utilisé à d’autres fins au niveau de la couche communication ... La méthode "appropriée" pour gérer la pagination consiste à utiliser les types que vous renvoyez. Plutôt que de renvoyer l'objet de questions, vous devriez plutôt renvoyer un nouveau type.
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 supports afin que vous puissiez faire de vos "pages" un format adapté à vos besoins. Si vous faites 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 cela est plus dans l'esprit de la spécification HTTP, plutôt que de modifier le paramètre Range pour autre chose.