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 GET
operación en la raíz de la colección si la colección es grande.
Como ejemplo, imagino que estoy exponiendo la Questions
tabla 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/questions
la API CRUD habitual 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 hacerlo, GET /db/questions
pero 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 Range
encabezado con una unidad de rango personalizado items
. 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í, 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
200
con unContent-Range
encabezado! - No creo que esto esté mal, pero preferiría un indicador más obvio de que la respuesta es solo Contenido parcial. - Devolución
400 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! Retorno
266 Partial Content
: se comporta exactamente igual que 206, pero responde a una solicitud que NO DEBE contener elRange
encabezado. 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?
fuente
Range = "Range" ":" ranges-specifier
dó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" ".Content-Range
encabezado se aplica al cuerpo (se puede usar con solicitud al cargar archivos grandes, etc., o como respuesta al descargar). ElRange
encabezado se utiliza para solicitar un cierto rango. Uno debe responder con206
cuándoRange
se incluyó el encabezado en la solicitud. Si no fue así, la respuesta aún puede incluir unContent-Range
encabezado, pero el código de respuesta debería ser200
. Este encabezado en realidad parece ideal para paginación.Respuestas:
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
, y206
solo 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
200
y enlaces de paginación potenciales). Ver RFC 4287 y RFC 5005 .fuente
items
unidad 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.Range
yContent-Range
con fines de búsqueda.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.
fuente
If-Unmodified-Since
, que corresponde a la variante E-TagIf-Match
, en lugar deIf-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)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 ).Todavía puede regresar
Accept-Ranges
yContent-Ranges
con un200
código de respuesta. Estos dos encabezados de respuesta le brindan suficiente información para inferir la misma información que un206
código de respuesta proporciona explícitamente.Lo usaría
Range
para paginación, y que simplemente devuelva un200
para un planoGET
.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
fuente
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
, regrese300 Multiple Choices
conLink
encabezados que especifiquen cómo llegar a cada página, así como un objeto JSON o página HTML con una lista de URL.Tendría un
Link
encabezado 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óximaLink
especificación . Esta relación explicaría su costumbre266
o su violación de206
. 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
2xx
có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 Choices
dice que DEBERÍA también proporcionar un cuerpo con una forma para que el agente de usuario elija. Si su cliente entiende, debe usar losLink
encabezados. 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/questions
o algo horrible como eso?Los comentarios sobre la publicación de Richard Levasseur me recuerdan una opción adicional: el
Accept
encabezado (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
, losLink
encabezados y la página HTML de un HTTP ingenuo inicialGET
, pero en lugar de rangos de uso, haga que su nueva relación paginación definir el uso de laAccept
cabecera. Su solicitud HTTP posterior podría verse así:El
Accept
encabezado 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 lopage
que significa el parámetro en el futuro.fuente
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.
fuente
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.
fuente
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.
fuente
Puede detectar el
Range
encabezado 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 unRange
encabezado. 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.fuente
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.
fuente
Con la publicación de rfc723x , las unidades de rango no registradas van en contra de una recomendación explícita en la especificación . Considere rfc7233 (despreciando rfc2616):
"Las nuevas unidades de rango deben registrarse con IANA " (junto con una referencia a un Registro de unidades de rango HTTP ).
fuente
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);
fuente
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.
fuente