Paginación en una colección de descanso

134

Estoy interesado en exponer una interfaz REST directa a colecciones de documentos JSON (piense en CouchDB o Persevere ). El problema con el que me encuentro es cómo manejar la GEToperación en la raíz de la colección si la colección es grande.

Como ejemplo, imagino que estoy exponiendo la Questionstabla de StackOverflow donde cada fila está expuesta como un documento (no es que exista necesariamente una tabla de este tipo, solo un ejemplo concreto de una colección considerable de 'documentos'). La colección se pondrá a disposición en /db/questionsla API CRUD habitual GET /db/questions/XXX, PUT /db/questions/XXX, POST /db/questionsestá en juego. La forma estándar de obtener toda la colección es hacerlo, GET /db/questionspero si ingenuamente vuelca cada fila como un objeto JSON, obtendrá una descarga bastante considerable y mucho trabajo por parte del servidor.

La solución es, por supuesto, la paginación. Dojo ha resuelto este problema en su JsonRestStore mediante una inteligente extensión compatible con RFC2616 de usar el Rangeencabezado con una unidad de rango personalizado items. El resultado es un 206 Partial Contentque devuelve solo el rango solicitado. La ventaja de este enfoque sobre un parámetro de consulta es que deja la cadena de consulta para ... consultas (por ejemplo, GET /db/questions/?score>200o somesuch, y sí, eso estaría codificado %3E).

Este enfoque cubre completamente el comportamiento que quiero. El problema es que RFC 2616 especifica que en una respuesta 206 (énfasis mío):

La solicitud DEBE haber incluido un campo de encabezado de Rango ( sección 14.35 ) que indica el rango deseado, y PUEDE haber incluido un campo de encabezado de Rango If ( sección 14.27 ) para hacer que la solicitud sea condicional.

Esto tiene sentido en el contexto del uso estándar del encabezado, pero es un problema porque me gustaría que la respuesta 206 sea la predeterminada para manejar clientes ingenuos / personas aleatorias que exploran.

He revisado el RFC en detalle buscando una solución, pero no estoy contento con mis soluciones y estoy interesado en que SO aborde el problema.

Ideas que he tenido:

  • ¡Regresa 200con un Content-Rangeencabezado! - No creo que esto esté mal, pero preferiría un indicador más obvio de que la respuesta es solo Contenido parcial.
  • Devolución400 Range Required : no hay un código de respuesta 400 especial para los encabezados necesarios, por lo que el error predeterminado debe usarse y leerse a mano. Esto también hace que la exploración a través del navegador web (o algún otro cliente como Resty) sea más difícil.
  • Use un parámetro de consulta : el enfoque estándar, pero espero permitir consultas a la Persevere y esto interrumpe el espacio de nombres de la consulta.
  • ¡Solo regresa 206! - Creo que la mayoría de los clientes no se asustarían, pero prefiero no ir en contra de un DEBE en el RFC
  • ¡Extiende la especificación! Retorno266 Partial Content : se comporta exactamente igual que 206, pero responde a una solicitud que NO DEBE contener el Rangeencabezado. Creo que 266 es lo suficientemente alto como para no tener problemas de colisión y tiene sentido para mí, pero no tengo claro si esto se considera tabú o no.

Creo que este es un problema bastante común y me gustaría ver que esto se haga de una manera de facto para que yo o alguien más no esté reinventando la rueda.

¿Cuál es la mejor manera de exponer una colección completa a través de HTTP cuando la colección es grande?

Karl Guertin
fuente
21
Wow, ese es un buen ejemplo de una pregunta en la que se han hecho algunos pensamientos serios antes.
Heiko Rupp
posible duplicado de paginación en una aplicación web REST
rds
1
En cuanto al enfoque de Dojo al usar el encabezado Range, aunque Accept-Ranges permite la extensión, por todo lo que puedo decir, el EBNF para Range no: tools.ietf.org/html/rfc2616#section-14.35.2 . La especificación indica Range = "Range" ":" ranges-specifierdónde esta última en tools.ietf.org/html/rfc2616#section-14.35.1 se describe simplemente como "byte- range -specifier", que debe comenzar con "bytes-unit", que se define como la cadena "bytes" ".
Brett Zamir
2
El Content-Rangeencabezado se aplica al cuerpo (se puede usar con solicitud al cargar archivos grandes, etc., o como respuesta al descargar). El Rangeencabezado se utiliza para solicitar un cierto rango. Uno debe responder con 206cuándo Rangese incluyó el encabezado en la solicitud. Si no fue así, la respuesta aún puede incluir un Content-Rangeencabezado, pero el código de respuesta debería ser 200. Este encabezado en realidad parece ideal para paginación.
Stijn de Witt
Pero el RFC 2616 dice que "las implementaciones de HTTP / 1.1 PUEDEN ignorar los rangos especificados usando otras unidades". Entonces, ¿es una buena práctica usar encabezados de rango para paginación? porque podría comprometer la interoperabilidad.
chetan choulwar

Respuestas:

23

Mi intuición es que las extensiones de rango HTTP no están diseñadas para su caso de uso, por lo que no debe intentarlo. Una respuesta parcial implica 206, y 206solo debe enviarse si el cliente lo solicitó.

Es posible que desee considerar un enfoque diferente, como el uso en Atom (donde la representación por diseño puede ser parcial y se devuelve con un estado 200y enlaces de paginación potenciales). Ver RFC 4287 y RFC 5005 .

Julian Reschke
fuente
14
El uso de Dojo está completamente dentro de las especificaciones. Si el servidor no comprende la itemsunidad de rango, devuelve una respuesta completa. Estoy familiarizado con Atom, pero esa no es la solución general para la paginación Rest. Esta no es una solución para un solo caso, más de lo que debería ser la solución general. No todos los documentos / colecciones se ajustan al modelo Atom y no hay razón para forzarlo a menos que sea necesario.
Karl Guertin
1
@KarlGuertin De acuerdo. Lástima que esta sea la respuesta aceptada, porque parece que muchos en la comunidad realmente están adoptando Rangey Content-Rangecon fines de búsqueda.
Stijn de Witt
34

Realmente no estoy de acuerdo con algunos de ustedes. He estado trabajando durante semanas en estas características para mi servicio REST. Lo que terminé haciendo es realmente simple. Mi solución solo tiene sentido para lo que las personas REST llaman una colección.

El cliente DEBE incluir un encabezado de "Rango" para indicar qué parte de la colección necesita, o estar preparado para manejar un error 413 ENTIDAD SOLICITADA DEMASIADO GRANDE cuando la colección solicitada es demasiado grande para ser recuperada en un solo viaje de ida y vuelta.

El servidor envía una respuesta 206 CONTENIDO PARCIAL, con el encabezado Content-Range que especifica qué parte del recurso se ha enviado, y un encabezado ETag para identificar la versión actual de la colección. Usualmente uso un ETag similar a Facebook {last_modification_timestamp} - {resource_id}, y considero que el ETag de una colección es el del recurso modificado más recientemente que contiene.

Para solicitar una parte específica de una colección, el cliente DEBE utilizar el encabezado "Rango" y completar el encabezado "If-Match" con el ETag de la colección obtenida de solicitudes realizadas previamente para adquirir otras partes de la misma colección. Por lo tanto, el servidor puede verificar que la colección no ha cambiado antes de enviar la parte solicitada. Si existe una versión más reciente, se devuelve una respuesta 412 PRECONDITION FAILED para invitar al cliente a recuperar la colección desde cero. Esto es necesario porque podría significar que algunos recursos podrían haberse agregado o eliminado antes o después de la parte solicitada actualmente.

Utilizo ETag / If-Match junto con Last-Modified / If-Unmodified-Since para optimizar el caché. Los navegadores y los servidores proxy pueden confiar en uno o en ambos para sus algoritmos de almacenamiento en caché.

Creo que una URL debe estar limpia a menos que incluya una consulta de búsqueda / filtro. Si lo piensa, una búsqueda no es más que una vista parcial de una colección. En lugar de los coches / search? Q = tipo de URL de BMW, deberíamos ver más coches? Manufacturer = BMW.

Mohamed
fuente
¿Quiso decir 416 "Rango solicitado no satisfecho" o "413" Entidad de solicitud demasiado grande?
1
@ Mohamed, creo que te refieres If-Unmodified-Since, que corresponde a la variante E-Tag If-Match, en lugar de If-Modified-Since. Dicho esto, también podría considerar eliminar esta restricción, dependiendo de su caso de uso. Supongamos que tiene una colección que solo crece desde la parte superior (como una colección de estilo "más nueva primero"), lo peor que puede suceder si esa colección cambia entre solicitudes es que un usuario que navega por una colección ve las entradas dos veces. (Lo cual en sí mismo también es una información útil: le dice al usuario que la colección ha cambiado)
Eugene Beresovsky
20
413 es "Solicitar entidad demasiado grande", no "Entidad solicitada demasiado grande". Significa que el tamaño de su solicitud, por ejemplo, al cargar un archivo, es mayor de lo que el servidor está dispuesto a procesar. Por lo tanto, usarlo para esto no parece ser completamente apropiado.
user247702
@Mohamed Sé que es una pregunta antigua, pero si el ETag de una colección es el ETag del recurso modificado más recientemente que contiene la colección, ¿qué valor del encabezado If-Match debe usarse al modificar un recurso en la colección? Usar el valor del ETag devuelto con la colección es incorrecto ya que el cliente podría modificar el recurso incluso si no ve el último estado del recurso.
Mickael Marrache
8
Estoy totalmente en desacuerdo sobre el uso 413. Este es un código de error que significa que el cliente está enviando algo que el servidor se niega a aceptar debido al tamaño. ¡No de la otra manera! Consulte tools.ietf.org/html/rfc7231#section-6.5.11 (tenga en cuenta que dice carga útil de solicitud . No carga útil de respuesta ).
exhuma
7

Todavía puede regresar Accept-Rangesy Content-Rangescon un 200código de respuesta. Estos dos encabezados de respuesta le brindan suficiente información para inferir la misma información que un 206código de respuesta proporciona explícitamente.

Lo usaría Rangepara paginación, y que simplemente devuelva un 200para un plano GET.

Esto se siente 100% DESCANSO y no dificulta la navegación.

Editar: escribí una publicación de blog sobre esto: http://otac0n.com/blog/2012/11/21/range-header-i-choose-you.html

John Gietzen
fuente
5

Si hay más de una página de respuestas y no desea ofrecer toda la colección a la vez, ¿eso significa que hay varias opciones?

En una solicitud /db/questions, regrese 300 Multiple Choicescon Linkencabezados que especifiquen cómo llegar a cada página, así como un objeto JSON o página HTML con una lista de URL.

Link: <>; rel="http://paged.collection.example/relation/paged"
Link: <>; rel="http://paged.collection.example/relation/paged"
...

Tendría un Linkencabezado para cada página de resultados (una cadena vacía significa la URL actual, y la URL es la misma para cada página, solo se accede con diferentes rangos), y la relación se define como personalizada según la próxima Linkespecificación . Esta relación explicaría su costumbre 266o su violación de 206. Estos encabezados son su versión legible por máquina, ya que todos sus ejemplos requieren un cliente comprensivo de todos modos.

(Si se apega a la ruta de "rango", creo que su propio 2xxcódigo de retorno, como lo describió, sería el mejor comportamiento aquí. Se espera que haga esto para sus aplicaciones y tales ["códigos de estado HTTP son extensibles. "], y tienes buenas razones.)

300 Multiple Choicesdice que DEBERÍA también proporcionar un cuerpo con una forma para que el agente de usuario elija. Si su cliente entiende, debe usar los Linkencabezados. Si se trata de un usuario que navega manualmente, ¿tal vez una página HTML con enlaces a un recurso raíz "paginado" especial que pueda manejar la representación de esa página en particular en función de la URL? /humanpage/1/db/questionso algo horrible como eso?


Los comentarios sobre la publicación de Richard Levasseur me recuerdan una opción adicional: el Acceptencabezado (sección 14.1). Cuando salió la especificación oEmbed, me preguntaba por qué no se había hecho completamente usando HTTP, y escribí una alternativa con ellos.

Mantenga las 300 Multiple Choices, los Linkencabezados y la página HTML de un HTTP ingenuo inicial GET, pero en lugar de rangos de uso, haga que su nueva relación paginación definir el uso de la Acceptcabecera. Su solicitud HTTP posterior podría verse así:

GET /db/questions HTTP/1.1
Host: paged.collection.example
Accept: application/json;PagingSpec=1.0;page=1

El Acceptencabezado le permite definir un tipo de contenido aceptable (su retorno JSON), además de parámetros extensibles para ese tipo (su número de página). Refiriéndose a mis notas de mi escrito de oEmbed (no puedo vincularlo aquí, lo enumeraré en mi perfil), podría ser muy explícito y proporcionar una versión de especificación / relación aquí en caso de que necesite redefinir lo pageque significa el parámetro en el futuro.

Vitorio
fuente
1
Encabezados de enlace +1, pero también recomendaría los primeros, previos, siguientes, últimos rels comunes, así como el archivo anterior, el siguiente archivo y el actual del RFC5005.
Joseph Holsten
> En una solicitud a / db / preguntas, devuelva 300 opciones múltiples con encabezados de enlace que especifiquen cómo llegar a cada página [..] El problema con eso (y con la mayoría de los diseños REST puros) es que mata por la latencia. El objetivo es minimizar las solicitudes de red. Esa primera solicitud debería arrojar resultados, no enlaces a más solicitudes que finalmente darán los datos que necesitamos.
Stijn de Witt
4

Editar:

Después de pensarlo un poco más, me inclino a aceptar que los encabezados de rango no son apropiados para la paginación. La lógica es que el encabezado Range está destinado a la respuesta del servidor, no a las aplicaciones. Si proporcionó 100 megabytes de resultados, pero el servidor (o cliente) solo podría procesar 1 megabyte a la vez, bueno, para eso está el encabezado Range.

También soy de la opinión de que un subconjunto de recursos es su propio recurso (similar al álgebra relacional), por lo que merece representación en la URL.

Básicamente, me retracto de mi respuesta original (a continuación) sobre el uso de un encabezado.


Creo que respondió su propia pregunta, más o menos: devuelva 200 o 206 con rango de contenido y, opcionalmente, use un parámetro de consulta. Detectaría el agente de usuario y el tipo de contenido y, dependiendo de ellos, buscaría un parámetro de consulta. De lo contrario, se requieren los encabezados de rango.

Básicamente, tiene objetivos en conflicto: permita que las personas usen su navegador para explorar (lo que no permite fácilmente encabezados personalizados), o obligue a las personas a usar un cliente especial que pueda establecer encabezados (que no les permite explorar).

Puede proporcionarles un cliente especial según la solicitud; si parece un navegador simple, envíe una pequeña aplicación ajax que muestre la página y establezca los encabezados necesarios.

Por supuesto, también existe el debate sobre si la URL debe contener todo el estado necesario para este tipo de cosas. Especificar el rango usando encabezados puede ser considerado "no reparador" por algunos.

Por otro lado, sería bueno que los servidores pudieran responder con un encabezado "Can-Specify: Header1, header2", y los navegadores web presentarían una interfaz de usuario para que los usuarios pudieran completar los valores, si así lo desean.

Richard Levasseur
fuente
Gracias por la respuesta. He pensado en el tema, pero esperaba obtener una segunda opinión. ¿Sucede tener un puntero para los argumentos del encabezado?
Karl Guertin
Aquí está el único que he marcado (vea la discusión en los comentarios): barelyenough.org/blog/2008/05/versioning-rest-web-services Otro sitio giraba en torno al uso de Ruby de .json, .xml,. El tipo de contenido de una solicitud. Algunos de los ejemplos: * idioma: ponerlo en la URL significa que enviar el enlace a otro país lo representaría en el idioma incorrecto. * paginación: ponerlo en el encabezado significa que no se puede vincular a las personas con lo que se ve
Richard Levasseur
* tipo de contenido: una combinación de problemas de lenguaje y paginación; si está en la URL, ¿qué sucede si el cliente no admite ese tipo de contenido (por ejemplo, una extensión .ajax y una extensión .html)? Por el contrario, sin ese tipo de contenido en la url, no se puede garantizar la misma representación. "nuevo sitio ajax! example.com/cool.ajax" vs "artículo genial aquí: example.com/article.ajax#id=123".
Richard Levasseur
2
OMI, si va en la URL o no depende de lo que es. Mi regla general es, si identificara un recurso concreto (ya sea un recurso en un estado específico, una selección de recursos o un resultado discreto), va en la URL. Las consultas de búsqueda, la paginación y las transacciones relajantes son buenos ejemplos de esto. Si es algo que se necesita para transformar la representación abstracta en una representación concreta, va en el encabezado. La información de autenticación y el tipo de contenido son buenos ejemplos de esto.
Richard Levasseur
Pienso en la cadena de consulta en una URL como opciones para consultar el recurso que se especifica.
wprl
3

Puede considerar usar un modelo similar al Protocolo de alimentación Atom ya que tiene un modelo HTTP de colecciones sensatas y cómo manipularlas (donde loco significa WebDAV).

Existe el Protocolo de publicación de Atom que define el modelo de colección y las operaciones REST, además de que puede usar RFC 5005 - Feed Paging and Archiving para navegar por grandes colecciones.

Cambiar de Atom XML a contenido JSON no debería afectar la idea.

dajobe
fuente
3

Creo que el verdadero problema aquí es que no hay nada en la especificación que nos diga cómo hacer redireccionamientos automáticos cuando nos enfrentamos a 413 - Entidad solicitada demasiado grande.

Estaba luchando con este mismo problema recientemente y busqué inspiración en el libro RESTful Web Services . Personalmente, no creo que 206 sea apropiado debido al requisito de encabezado. Mis pensamientos también me llevaron a 300, pero pensé que era más para diferentes tipos de mimos, así que busqué lo que Richardson y Ruby tenían que decir sobre el tema en el Apéndice B, página 377. Sugieren que el servidor simplemente elija el preferido representación y enviarlo de vuelta con un 200, básicamente ignorando la noción de que debería ser un 300.

Eso también concuerda con la noción de enlaces a los próximos recursos que tenemos de atom. La solución que implementé fue agregar las teclas "siguiente" y "anterior" al mapa json que estaba enviando de vuelta y listo.

Más tarde, comencé a pensar que tal vez lo que hay que hacer es enviar un 307 - Redirección temporal a un enlace que sería algo así como / db / preguntas / 1,25 - que deja el URI original como el nombre del recurso canónico, pero lo lleva a un recurso subordinado apropiadamente nombrado. Este es un comportamiento que me gustaría ver en un 413, pero 307 parece un buen compromiso. Sin embargo, todavía no he probado esto en código. Lo que sería aún mejor es que la redirección redirija a una URL que contenga los ID reales de las preguntas más recientes. Por ejemplo, si cada pregunta tiene una ID entera, y hay 100 preguntas en el sistema y desea mostrar las diez más recientes, las solicitudes a / db / preguntas deben ser 307 a / db / preguntas / 100,91

Esta es una muy buena pregunta, gracias por hacerla. Me confirmaste que no estoy loco por haber pasado días pensando en ello.

stinkymatt
fuente
303 sería mejor a este respecto que 307. 307 implica que la URL original pronto comenzará a responder como el cliente espera.
Nicholas Shanks el
RFC 7231 hace referencia al código de estado HTTP 413 como Carga útil demasiado grande y relaciona este código con el tamaño de la solicitud y no con el tamaño de la respuesta potencial.
beawolf
1

Puede detectar el Rangeencabezado e imitar Dojo si está presente, e imitar Atom si no lo está. Me parece que esto divide claramente los casos de uso. Si está respondiendo a una consulta REST desde su aplicación, espera que esté formateada con un Rangeencabezado. Si está respondiendo a un navegador informal, si devuelve enlaces de paginación, permitirá que la herramienta brinde una manera fácil de explorar la colección.

Greg
fuente
1

Uno de los grandes problemas con los encabezados de rango es que muchos proxies corporativos los filtran. Aconsejaría usar un parámetro de consulta en su lugar.

usuario64141
fuente
0

Me parece que la mejor manera de hacer esto es incluir el rango como parámetros de consulta. por ejemplo, GET / db / preguntas /? date> mindate & date <maxdate . Al OBTENER el / db / preguntas / sin parámetros de consulta, devuelva 303 con Ubicación: / db / preguntas /? Query-parámetros-para-recuperar-la-página-predeterminada . Luego, proporcione una URL diferente por la cual quien esté consumiendo su API para obtener estadísticas sobre la colección (por ejemplo, qué parámetros de consulta usar si quiere la colección completa);

Dathan
fuente
0

Si bien es posible usar el encabezado Range para este propósito, no creo que esa sea la intención. Parece haber sido diseñado para manejar conexiones débiles, así como para limitar los datos (por lo que el cliente puede solicitar parte de la solicitud si falta algo o si el tamaño es demasiado grande para procesar). Está pirateando la paginación en algo que probablemente se usa para otros fines en la capa de comunicación. La forma "adecuada" de manejar la paginación es con los tipos que devuelve. En lugar de devolver el objeto de preguntas, debería devolver un nuevo tipo.

Entonces, si las preguntas son así:

<questions> <question index=1></question> <question index=2></question> ... </questions>

El nuevo tipo podría ser algo como esto:

<questionPage> <startIndex>50</startIndex> <returnedCount>10</returnedCount> <totalCount>1203</totalCount> <questions> <question index=50></question> <question index=51></question> .. </questions> <questionPage>

Por supuesto, usted controla sus tipos de medios, por lo que puede hacer que sus "páginas" tengan un formato que se adapte a sus necesidades. Si cree que es algo genérico, puede tener un solo analizador en el cliente para manejar la paginación igual para todos los tipos. Creo que eso está más en el espíritu de la especificación HTTP, en lugar de falsificar el parámetro Range para otra cosa.

jeremyh
fuente