pageable page example http rest http-headers pagination

http - example - spring pageable controller



Paginación en una colección de descanso (12)

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

Como ejemplo pretendo exponer la tabla de Questions de StackOverflow donde cada fila está expuesta como un documento (no es que necesariamente exista tal tabla, solo un ejemplo concreto de una considerable colección de ''documentos''). La colección estará disponible en /db/questions con la API usual apr GET /db/questions/XXX , PUT /db/questions/XXX , POST /db/questions está en juego. La forma estándar de obtener toda la colección es GET /db/questions pero si eso 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, paginación. Dojo ha resuelto este problema en su JsonRestStore través de una inteligente extensión compatible con RFC2616 que utiliza el encabezado de Range con items unidad de rango personalizados. El resultado es un 206 Partial Content que 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>200 o somesuch, y sí que se codificaría %3E ).

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

La solicitud DEBIÓ haber incluido un campo de encabezado Rango ( sección 14.35 ) que indica el rango deseado, y PUEDE haber incluido un campo de encabezado If-Range ( 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 / explorar personas al azar.

Revisé el RFC en detalle buscando una solución, pero no estoy satisfecho con mis soluciones y estoy interesado en que SO tome el problema.

Ideas que he tenido:

  • ¡Devuelve 200 con un encabezado Content-Range ! - No creo que esto esté mal, pero preferiría un indicador más obvio de que la respuesta es solo contenido parcial.
  • Retorno 400 Range Required : no hay un código de respuesta 400 especial para los encabezados requeridos, 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 (u 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 corta en el espacio de nombres de la consulta.
  • ¡Solo devuelve 206 ! - Creo que la mayoría de los clientes no se asustarían, pero preferiría no ir contra un MUST en el RFC
  • Extiende la especificación! Devuelve 266 Partial Content : se comporta exactamente como 206, pero responde a una solicitud que NO DEBE contener el encabezado de Range . Me imagino 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 así que yo o alguien más no estoy 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?


Aún puede devolver Accept-Ranges y Content-Ranges con un código de respuesta de 200 . Estos dos encabezados de respuesta le brindan suficiente información para inferir la misma información que un código de respuesta 206 proporciona explícitamente.

Usaría Range para la paginación, y simplemente devolvería 200 para un GET simple.

Esto se siente 100% RESTful y no hace la navegación más difícil.

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



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

Estuve 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 mímica, 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, ignorando básicamente 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 átomo. La solución que implementé fue agregar las claves "siguiente" y "anterior" al mapa json que estaba enviando y terminar con él.

Más tarde comencé a pensar que tal vez lo que se debe hacer es enviar un 307 - Redireccionamiento temporal a un enlace que sería algo así como / db / questions / 1,25 - que deja el URI original como el nombre del recurso canónico, pero te lleva a un recurso subordinado apropiadamente nombrado. Este es el comportamiento que me gustaría ver de un 413, pero 307 parece un buen compromiso. Sin embargo, todavía no lo he probado en el código. Lo que sería aún mejor es redirigir 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 / questions deben ser de 307 a / db / questions / 100,91

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


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 del rango está destinado a la respuesta del servidor, no a las aplicaciones. Si sirvió 100 megabytes de resultados, pero el servidor (o cliente) solo pudo procesar 1 megabyte a la vez, bueno, para eso es el encabezado del rango.

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 una representación en la URL.

Entonces, básicamente, me retracto de mi respuesta original (abajo) sobre el uso de un encabezado.

Creo que respondiste tu propia pregunta, más o menos: devuelve 200 o 206 con rango de contenido y, opcionalmente, usa un parámetro de consulta. Me gustaría oler el agente de usuario y el tipo de contenido y, dependiendo de esos, buscar un parámetro de consulta. De lo contrario, requiere los encabezados de rango.

En esencia, tiene objetivos conflictivos: permita que las personas usen su navegador para explorar (lo que no permite encabezados personalizados fácilmente) u obligue a las personas a usar un cliente especial que pueda establecer encabezados (lo que no les permite explorar).

Simplemente puede proporcionarles el cliente especial dependiendo de la solicitud: si se ve como un navegador simple, envíe una pequeña aplicación ajax que represente 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 si los servidores pudieran responder con un encabezado "Can-Specify: Header1, header2", y los navegadores web presentarían una IU para que los usuarios puedan completar los valores, si así lo desean.


Me parece que la mejor manera de hacerlo es incluir el rango como parámetros de consulta. por ejemplo, GET / db / questions /? date> mindate & date <maxdate . Tras un GET a / db / questions / sin parámetros de consulta, devuelva 303 con Location: / db / questions /? Query-parameters-to-retrieve-the-default-page . A continuación, proporcione una URL diferente por la que cualquiera que consuma su API obtenga estadísticas sobre la colección (por ejemplo, qué parámetros de consulta usar si desea la colección completa);


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

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


Puede considerar usar un modelo como el Atom Feed Protocol, ya que tiene un modelo de colecciones de HTTP sensato y cómo manipularlas (donde locura significa WebDAV).

Existe el protocolo de publicación Atom que define el modelo de recopilación y las operaciones REST además de que puede usar RFC 5005 - Feed paging y archiving para recorrer grandes colecciones.

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


Puedes detectar el encabezado del Range e imitar a Dojo si está presente e imitar a 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 encabezado de Range . Si está respondiendo a un navegador informal, entonces si devuelve enlaces de búsqueda, la herramienta le brindará una manera fácil de explorar la colección.


Realmente no estoy de acuerdo con algunos de ustedes, chicos. He estado trabajando durante semanas en estas funciones 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 "Rango" para indicar qué parte de la colección necesita, o de lo contrario estar preparado para manejar un error 413 DETALLE ENTIDAD 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. Normalmente uso un ETag similar a Facebook {last_modification_timestamp} - {resource_id}, y considero que el ETag de una colección es el recurso modificado más reciente que contiene.

Para solicitar una parte específica de una colección, el cliente DEBE usar el encabezado "Rango" y rellenar el encabezado "Si coinciden" con la ETag de la recopilación obtenida de las solicitudes realizadas anteriormente para adquirir otras partes de la misma colección. Por lo tanto, el servidor puede verificar que la colección no haya cambiado antes de enviar la porción 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.

Yo uso ETag / If-Match en tándem con Last-Modified / If-Unmodified-Since para optimizar el caché. Los navegadores y los proxies pueden confiar en uno o ambos para sus algoritmos de almacenamiento en caché.

Creo que una URL debe estar limpia a menos que sea para incluir una consulta de búsqueda / filtro. Si lo piensas, una búsqueda no es más que una vista parcial de una colección. En lugar de los autos / search? Q = Tipo de URL de BMW, deberíamos ver más automóviles? Manufacturer = BMW.


Si bien es posible utilizar el encabezado de rango para este fin, no creo que esa fue la intención. Parece que se diseñó para manejar conexiones escamosas y limitar los datos (para que el cliente pueda solicitar parte de la solicitud si falta algo o si el tamaño es demasiado grande para procesarlo). Está pirateando la paginación en algo que probablemente se use para otros fines en la capa de comunicación. La forma "correcta" de manejar la paginación es con los tipos que devuelve. En lugar de devolver el objeto a las preguntas, deberías devolver un nuevo tipo en su lugar.

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, para que pueda convertir sus "páginas" en un formato que se adapte a sus necesidades. Si cree que es algo genérico, puede tener un analizador único en el cliente para gestionar el paginado de la misma manera para todos los tipos. Creo que eso está más en el espíritu de la especificación HTTP, en lugar de cambiar el parámetro Range por otra cosa.


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

En una solicitud a /db/questions , devuelva 300 Multiple Choices con encabezados de Link que especifican 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 encabezado de Link para cada página de resultados (una cadena vacía significa la URL actual, y la URL es la misma para cada página, a la que se accede con diferentes rangos), y la relación se define como una personalizada según el próximo Especificación de Link Esta relación explicaría su 266 personalizada o su violación de 206 . Estos encabezados son su versión legible por máquina, ya que todos sus ejemplos requieren un cliente de comprensión de todos modos.

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

300 Multiple Choices dice que también DEBERÍA proporcionar un cuerpo con una forma de selección para el agente de usuario. Si su cliente lo entiende, debería usar los encabezados de Link . Si se trata de un usuario que navega de forma manual, tal vez una página HTML con enlaces a un recurso raíz especial "paginado" que pueda manejar el procesamiento de esa página en particular según la URL. /humanpage/1/db/questions o algo horrible como ese?

Los comentarios sobre la publicación de Richard Levasseur me recuerdan a una opción adicional: el encabezado Accept (sección 14.1). Cuando salió la especificación oEmbed, me pregunté por qué no se había hecho completamente con HTTP y redacté una alternativa para usarlos.

Mantenga las 300 Multiple Choices , los encabezados de Link y la página HTML para un HTTP GET inicial ingenuo, pero en lugar de usar rangos, haga que su nueva relación de búsqueda defina el uso del encabezado Accept . 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 encabezado Accept le permite definir un tipo de contenido aceptable (su retorno JSON), más los parámetros extensibles para ese tipo (su número de página). Rifiando mis notas de mi escritura de OEmbed (no se puede vincular aquí, lo voy a enumerar en mi perfil), podría ser muy explícito y proporcionar aquí una versión de relación / especificación en caso de que necesite redefinir cuál es el parámetro de la page significa en el futuro.


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