Cursores estables para paginar resultados que pueden cambiar mientras se leen
Si una colección cambia entre la primera y la última página, el offset puede repetir u omitir resultados. Un cursor ligado a una revisión conserva un corte auditable.
Por Marco Ferreiro ·
Una API entrega la primera página de infracciones y, antes de que el cliente pida la segunda, ingresa un acta nueva y se corrige otra. Si el cliente usa offset=50, la posición 50 puede ya no representar el mismo borde. El resultado será una fila repetida, una omitida o una mezcla imposible de reproducir.
El cursor estable resuelve el problema sólo cuando identifica tanto la posición como el corte de actas que se está recorriendo.
Por qué el offset se mueve
Supongamos un listado de actas ordenado por observed_at DESC, id DESC, con páginas de tres registros. Al iniciar existen F9, F8, F7, F6, F5, F4.
La primera página devuelve F9, F8, F7. Antes de pedir offset=3, entra F10 al comienzo. La nueva lista es F10, F9, F8, F7, F6, F5, F4. El offset tres devuelve F7, F6, F5: F7 se repite y F4 queda para otra página.
No es un error matemático del cliente. El índice numérico fue aplicado a dos versiones distintas del listado de infracciones.
Posición y revisión son dos datos diferentes
Un cursor robusto puede representar:
- la revisión o instantánea
R42; - los valores de orden del último elemento entregado;
- tamaño de página autorizado;
- filtros y tenant vinculados;
- versión del formato;
- vencimiento e integridad.
El cliente no necesita leer esos campos. Recibe una cadena opaca y la devuelve sin modificar. El servidor valida que pertenece a la misma flota, consulta, filtros y revisión.
Codificar sólo F7 permite continuar “después de F7” sobre el estado actual, pero no garantiza que una corrección haya conservado el orden ni que un acta retirada siga siendo localizable.
Dos modelos válidos de estabilidad
El primer modelo conserva una instantánea materializada. Todas las páginas leen la misma revisión hasta completar o vencer. Ofrece semántica clara, pero consume almacenamiento y exige una política de retención.
El segundo usa un corte temporal o de versión: por ejemplo, sólo filas cuya revisión sea menor o igual a R42, con historial suficiente para reconstruir valores. Puede ser más barato si el sistema ya versiona cambios, pero un campo mutable sin historial rompe la promesa.
En ambos casos debe existir un orden total. Ordenar sólo por fecha de infracción permite empates; agregar un identificador estable como segundo criterio evita que dos filas intercambien posición arbitrariamente.
Cómo publicar la página siguiente
RFC 8288 define enlaces tipados y su serialización en el encabezado HTTP Link. Una respuesta puede publicar la URI siguiente con relación next, además de incluirla en el cuerpo si el contrato lo necesita. RFC 8977 muestra una aplicación concreta de enlaces para paginar resultados RDAP.
La API debería devolver el enlace completo o el cursor opaco, no pedir al cliente que reconstruya parámetros internos. OpenAPI permite documentar el esquema de la respuesta, ejemplos y errores esperados.
Una forma posible es:
{
"revision": "R42",
"items": [
{ "id": "F9" },
{ "id": "F8" },
{ "id": "F7" }
],
"nextCursor": "opaque-value",
"hasMore": true
}
Los identificadores son ficticios. El contrato público no debería revelar cómo firmar o decodificar el cursor.
Qué papel cumplen ETag y HTTP condicional
RFC 9110 define validadores como ETag y solicitudes condicionales. Un ETag puede identificar la representación de una página o la revisión del conjunto y ayudar a detectar cambios. No reemplaza automáticamente al cursor: su semántica debe quedar documentada.
Por ejemplo, la primera respuesta puede declarar un ETag de revisión. Si el cliente reanuda con un cursor incompatible, el servidor rechaza la solicitud en vez de devolver silenciosamente otro conjunto de actas. Para descargas largas, también puede permitir verificar que una representación en caché sigue vigente.
No conviene usar una fecha de servidor redondeada como único validador si dos cambios pueden ocurrir dentro de ese intervalo.
Altas, correcciones y retiros durante la lectura
En la revisión R42, un acta nueva pertenece a R43 y no aparece a mitad del recorrido. Una corrección posterior tampoco debe cambiar la posición histórica ya entregada; se verá al iniciar una nueva lectura. Si un acta fue retirada por una corrección sensible, la política puede invalidar la revisión completa en lugar de seguir sirviéndola.
La consistencia no significa ocultar novedades para siempre. Significa elegir un corte explícito. Al terminar, el cliente puede iniciar otra enumeración o consumir eventos desde R42 para conocer cambios.
Errores que forman parte del contrato
Definí respuestas distintas para cursor mal formado, firma inválida, filtros incompatibles, revisión vencida y acceso a otro tenant. No conviertas todos los casos en una página vacía: una lista vacía podría interpretarse como que la flota no tiene multas.
También fijá un máximo de tamaño, duración de revisión y comportamiento de reintentos. Los logs deben usar identificadores opacos; no incluyas patentes en el cursor ni en métricas.
Cómo probarlo de manera reproducible
El test de contrato crea un listado de actas ficticias, obtiene la primera página, inserta y corrige filas y continúa con ambos métodos. La aserción no es que el cursor devuelva la versión más nueva: debe devolver cada elemento de la revisión inicial exactamente una vez y en el orden documentado.
Después se inicia un recorrido nuevo y se comprueba que incluye las actas nuevas. Finalmente, se prueba expiración, cambio de filtro, cursor alterado y aislamiento entre empresas.
Un cursor estable no es una cadena misteriosa: es una promesa de consistencia. Cuando esa promesa incluye revisión, orden y errores explícitos, una integración puede auditar qué actas leyó incluso mientras el sistema siguió cambiando.
Metodología
- Alcance
- Comparación lógica de paginación por offset y cursor sobre una colección sintética que recibe altas y correcciones durante la lectura.
- Unidad de análisis
- Una ejecución completa de paginado identificada por revisión, orden total, tamaño de página y secuencia de enlaces siguientes.
- Cobertura
- Semántica de enlaces y validadores basada en RFC 8288, RFC 9110 y un ejemplo estandarizado de paginación en RFC 8977.
Limitaciones
- Los estándares citados no imponen un algoritmo universal de cursor ni una duración de instantánea.
- La implementación debe elegir consistencia, retención y costo según el almacenamiento y el volumen de la integración.
Fuentes
- RFC 8288 — Web Linking — RFC Editor (consultada el )
- RFC 9110 — HTTP Semantics — RFC Editor (consultada el )
- RFC 8977 — RDAP sorting and paging parameters — RFC Editor (consultada el )
- OpenAPI Specification 3.1.1 — OpenAPI Initiative (consultada el )
Preguntas frecuentes
¿Un cursor es simplemente el último identificador recibido?
Puede contener esa posición, pero no alcanza si la colección cambia. Para una lectura estable también debe quedar ligada una revisión, instantánea o regla de corte que el servidor pueda aplicar en páginas posteriores.
¿El cursor debería ser legible y editable por el cliente?
No hace falta. Conviene tratarlo como opaco, validarlo en el servidor y proteger su integridad. El contrato documenta cómo usarlo y sus errores, no su representación interna.
¿Qué debe ocurrir cuando una revisión ya no está disponible?
La API debe responder un error explícito de cursor vencido o inválido e indicar cómo reiniciar. Continuar sobre otra revisión sin avisar mezcla cortes y vuelve imposible auditar la enumeración.
Notas relacionadas
Cómo versionar una respuesta cuando cada jurisdicción cambia
Una respuesta agregada necesita distinguir versión de contrato, revisión de datos y estado de cada fuente. Así un cambio municipal no obliga a reinterpretar silenciosamente todo el resultado.
Webhooks de nuevas multas: idempotencia, reintentos y trazabilidad
Un webhook confiable puede llegar más de una vez, demorarse o fallar después de ser procesado. El diseño debe deduplicar eventos, reintentar con límites y conservar evidencia de punta a punta.
Cobertura de una API: cómo comunicar fuentes caídas y resultados parciales
Una integración de infracciones necesita informar qué fuentes respondió, cuáles fallaron y si el resultado es completo. Un cero sin cobertura comprobada puede inducir decisiones incorrectas.