Implementa GET condicional enviando validadores estables con cada documento, comprobando los validadores guardados por el cliente y devolviendo el documento actual o un 304 Not Modified sin cuerpo. Este tutorial para Mac muestra el proceso con una página de texto sintética.

La tarea consiste en hacer que las solicitudes repetidas de un documento público transfieran su contenido solo cuando sea necesario. Ejecutarás un pequeño servidor Python local, lo probarás desde Terminal y simularás una actualización del documento. El ejemplo se mantiene en local; publicarlo es una tarea aparte.

¿Cómo debe elegir el servidor entre 200 y 304?

Una solicitud inicial recibe 200 OK, el cuerpo del documento y sus validadores. Una solicitud condicional posterior envía los validadores guardados. Si el documento no ha cambiado, el servidor devuelve 304 Not Modified, lo que permite al cliente reutilizar el cuerpo almacenado. Si el contenido cambia, se recibe una nueva respuesta 200. Solicitudes condicionales HTTP.

Para un documento existente que, de otro modo, devolvería 200, aplica esta secuencia de decisión. If-None-Match tiene prioridad sobre If-Modified-Since; no son dos pruebas independientes. RFC 9110, campos de solicitud condicional.

Condición de la solicitud Respuesta
Sin encabezados condicionales 200 con el documento
If-None-Match coincide con la etiqueta actual 304, sin cuerpo
If-None-Match está presente, pero no coincide 200, aunque una fecha proporcionada indique que no hay cambios
Solo se proporciona un If-Modified-Since válido y el documento no es más reciente 304, sin cuerpo
Solo se proporciona un If-Modified-Since válido y el documento es más reciente 200 con el documento

Evalúa estas condiciones después de seleccionar el documento y establecer su respuesta normal. Un documento inexistente debe seguir generando la respuesta habitual de recurso no encontrado. Un 304 no puede contener contenido ni tráileres. Debe incluir los metadatos de caché aplicables, incluidos los campos Date, ETag, Vary, Cache-Control, Expires y Content-Location que acompañarían a su respuesta 200. RFC 9110, evaluación de condiciones previas y requisitos de 304.

Elige validadores que describan el documento publicado

Un ETag es un identificador de versión entre comillas, como "doc-v1". Su método de generación se define según la implementación: puede servir un identificador de revisión o un hash del contenido. Mantenlo estable mientras la representación sea la misma y cámbialo cuando esta cambie. El ejemplo usa dos revisiones explícitas con contenido fijo. Encabezado ETag.

Una etiqueta fuerte identifica una representación específica; una etiqueta débil tiene el prefijo W/ y puede describir contenido semánticamente equivalente cuyos bytes difieren. Las comparaciones GET condicionales con If-None-Match usan comparación débil, por lo que W/"doc-v1" coincide con "doc-v1". Las solicitudes también pueden incluir una lista de etiquetas o el comodín independiente *. RFC 9110, If-None-Match.

Last-Modified indica cuándo cambió el documento. Usa una hora de modificación determinada de forma coherente e incluye los componentes pertinentes del documento, en lugar de usar la hora de cada solicitud. No debe ser posterior a la Date de la respuesta. RFC 9110, generación de Last-Modified.

El ejemplo asigna horas de publicación deliberadamente ilustrativas a sus dos revisiones sintéticas: las doce del mediodía GMT del 1 y el 2 de enero de 2024. Esas fechas son datos de prueba. En un flujo de publicación real, usa metadatos de publicación mantenidos. Last-Modified es menos preciso que un ETag para detectar diferencias de contenido y ofrece una alternativa útil. Encabezado Last-Modified.

Elige una política de caché por separado

Los validadores indican si una copia almacenada sigue coincidiendo. La política de frescura determina cuándo la caché debe consultar al servidor. En este tutorial se envía Cache-Control: no-cache, que permite almacenar la respuesta, pero exige validarla correctamente antes de reutilizarla. Así es fácil observar el comportamiento de las solicitudes repetidas. no-store prohíbe el almacenamiento, mientras que max-age permite reutilizar la respuesta mientras siga estando fresca. RFC 9111, directivas de respuesta de caché.

Para un documento público que tolere un breve retraso antes de que se vea una actualización, podría ser adecuada una política como max-age=60. Es una decisión editorial ilustrativa: elige el intervalo según los requisitos de actualización del documento. Una respuesta almacenada que siga fresca puede reutilizarse sin contactar con el servidor, por lo que no todas las visitas repetidas tienen que producir un 304. RFC 9111, frescura.

Ejecuta un ejemplo local completo en tu Mac

Abre Terminal y comprueba los requisitos previos con los siguientes comandos. Usa Python 3.9 o posterior porque el programa utiliza str.removeprefix. Si python3 no está disponible, instala Python 3 antes de continuar. No se necesitan paquetes de Python de terceros.

python3 --version
curl --version

En una carpeta de trabajo nueva, guarda el siguiente programa completo como server.py con un editor de código de texto sin formato. Sirve únicamente /page.txt, implementa GET y HEAD, y selecciona una revisión del documento a partir del argumento de línea de comandos. BaseHTTPRequestHandler de Python dirige las solicitudes a los métodos correspondientes y proporciona utilidades para escribir respuestas. Documentación del servidor HTTP de Python.

import argparse
import re
from datetime import datetime, timezone
from email.utils import format_datetime, parsedate_to_datetime
from http.server import BaseHTTPRequestHandler, HTTPServer

parser = argparse.ArgumentParser()
parser.add_argument("version", type=int, choices=(1, 2))
args = parser.parse_args()

BODY = {
    1: b"Paper Shapes\nA circle and a square.\n",
    2: b"Paper Shapes\nA circle, a square, and a triangle.\n",
}[args.version]
ETAG = '"doc-v%d"' % args.version
MODIFIED = datetime(2024, 1, args.version, 12, tzinfo=timezone.utc)
LAST_MODIFIED = format_datetime(MODIFIED, usegmt=True)
TOKEN = r'(?:W/)?"[\x21\x23-\x7e\x80-\xff]*"'
TAG_LIST = re.compile(rf'[ \t]*(?:{TOKEN})?(?:[ \t]*,[ \t]*(?:{TOKEN})?)*[ \t]*')

def tag_matches(value):
    if value.strip() == "*":
        return True
    if not TAG_LIST.fullmatch(value):
        return False
    return any(tag.removeprefix("W/") == ETAG
               for tag in re.findall(TOKEN, value))

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.serve_document()

    def do_HEAD(self):
        self.serve_document()

    def serve_document(self):
        if self.path != "/page.txt":
            self.send_error(404)
            return
        not_modified = False
        tags = self.headers.get_all("If-None-Match")
        if tags is not None:
            not_modified = tag_matches(",".join(tags))
        else:
            dates = self.headers.get_all("If-Modified-Since", [])
            if len(dates) == 1:
                try:
                    since = parsedate_to_datetime(dates[0])
                    if since.tzinfo is None:
                        since = since.replace(tzinfo=timezone.utc)
                    not_modified = MODIFIED <= since
                except (TypeError, ValueError, OverflowError):
                    pass
        self.send_response(304 if not_modified else 200)
        self.send_header("ETag", ETAG)
        self.send_header("Cache-Control", "no-cache")
        if not not_modified:
            self.send_header("Last-Modified", LAST_MODIFIED)
            self.send_header("Content-Type", "text/plain; charset=utf-8")
            self.send_header("Content-Length", str(len(BODY)))
        self.end_headers()
        if not not_modified and self.command == "GET":
            self.wfile.write(BODY)

if __name__ == "__main__":
    HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()

Esta es una demostración local de los dos validadores mencionados, no una pila completa para servir HTTP. La conversión de fechas usa el analizador permisivo de fechas de correo electrónico de Python; una implementación desplegada debería usar validación de fechas específica de HTTP y gestionar las solicitudes condicionales. La documentación de Python describe las entradas aceptadas por ese analizador y sus excepciones. Utilidades de fecha de correo electrónico de Python.

En Terminal, cambia a la carpeta que contiene server.py e inicia la revisión 1. Deja esa ventana en ejecución. Si el puerto 8000 ya está ocupado, elige otro puerto disponible en el programa y usa el mismo puerto en todas las URL de prueba.

python3 server.py 1

Prueba el contenido sin cambios, el contenido modificado y la prioridad

Abre otra ventana de Terminal. Las siguientes solicitudes muestran los encabezados de respuesta y cualquier cuerpo devuelto. La opción -i incluye los encabezados, -H proporciona un encabezado de solicitud y --max-time limita la duración de cada transferencia. Manual de curl.

Establece la respuesta inicial

curl --max-time 5 -sS -i http://127.0.0.1:8000/page.txt

Espera 200 OK, el texto de la página sintética, ETag: "doc-v1" y Last-Modified: Mon, 01 Jan 2024 12:00:00 GMT. Si aplicas este procedimiento a otro documento, anota los valores que realmente se devuelvan; los validadores corresponden a esa respuesta concreta.

Revalida con cada validador

curl --max-time 5 -sS -i \
  -H 'If-None-Match: "doc-v1"' \
  http://127.0.0.1:8000/page.txt

curl --max-time 5 -sS -i \
  -H 'If-Modified-Since: Mon, 01 Jan 2024 12:00:00 GMT' \
  http://127.0.0.1:8000/page.txt

Ambas solicitudes deberían devolver 304 Not Modified sin el texto de la página. La respuesta conserva ETag y Cache-Control; la utilidad de respuesta de Python añade Date. Repetir la primera solicitud con W/"doc-v1" también debería devolver 304. El ejemplo analiza las etiquetas entre comillas en vez de dividirlas sin más por comas, porque una etiqueta puede contener una coma.

Comprueba que ETag tiene prioridad

curl --max-time 5 -sS -i \
  -H 'If-None-Match: "older-revision"' \
  -H 'If-Modified-Since: Wed, 03 Jan 2024 12:00:00 GMT' \
  http://127.0.0.1:8000/page.txt

Espera 200 OK con el documento. La etiqueta no coincide, así que la fecha posterior no debe activar un 304. Esta solicitud deliberadamente contradictoria detecta una implementación que combina las dos pruebas por error.

Simula la publicación de la siguiente revisión

En la ventana del servidor, pulsa Control-C y luego inicia la revisión 2. Mantén la URL de solicitud sin cambios.

python3 server.py 2

Repite las dos solicitudes de revalidación anteriores que contienen los validadores de la revisión 1. Ambas deberían recibir ahora 200, la frase actualizada que menciona un triángulo, ETag: "doc-v2" y la fecha de modificación del 2 de enero. Repetir una solicitud con If-None-Match: "doc-v2" debería devolver 304.

Estos resultados forman una secuencia de aceptación: descarga inicial, validación sin cambios, validadores contradictorios, documento modificado y validación sin cambios de la nueva revisión. El ejemplo se probó con 24 comprobaciones locales en ambas revisiones, incluidas etiquetas débiles, listas de etiquetas, coincidencia con comodines, HEAD y rutas inexistentes.

Por qué las solicitudes de los navegadores y Google pueden parecer distintas

Los navegadores suelen gestionar los validadores HTTP mediante su caché. El algoritmo Fetch de WHATWG añade los validadores guardados al revalidar y, tras un 304 satisfactorio, actualiza los encabezados de la respuesta almacenada y la devuelve. Por eso, el JavaScript de la aplicación puede recibir una respuesta 200 utilizable y el cuerpo del documento aunque la revalidación de red haya recibido 304. Estándar Fetch, obtención por red HTTP o caché.

Al diagnosticar el comportamiento del servidor, empieza con las solicitudes explícitas de curl anteriores. Proporcionar manualmente encabezados condicionales en JavaScript del navegador cambia la prueba: Fetch cambia a no-store una solicitud con el modo de caché predeterminado que contiene esos encabezados. Deja que el navegador gestione su caché cuando pruebes la revalidación habitual del navegador. Estándar Fetch, modo de caché de la solicitud.

La infraestructura de rastreo de Google admite ambos pares de validadores y recomienda ETags porque evitan problemas de formato de fecha. Su documentación también indica que esa infraestructura no admite otras directivas de caché HTTP. Elige la política de caché del navegador por separado de las expectativas sobre el comportamiento de Google al volver a rastrear. Descripción general de los rastreadores y clientes de recuperación de Google.

Google no envía encabezados condicionales en cada intento de rastreo. La documentación de Búsqueda explica que un 304 permite reutilizar el contenido rastreado anteriormente, mientras que el contenido más reciente puede recibir un 200 con el cuerpo actualizado. Mantén operativas las solicitudes normales sin condiciones; que una solicitud del rastreador no incluya validadores no demuestra que tu implementación haya fallado. Solucionar errores de rastreo de Google Search.

Antes de aplicar este patrón a un documento publicado, repite la secuencia de aceptación en su URL de servicio real. Comprueba que las revisiones sin cambios conservan sus validadores, que las ediciones los actualizan y que un 304 no lleva cuerpo del documento. El resultado útil es reutilizar correctamente el documento y mantener accesible el contenido modificado.

Fuentes