El deploy en verde y el HTML viejo: depuré la cache del edge de Cloudflare · Daniel Tinizaray

El deploy en verde y el HTML viejo: depuré la cache del edge de Cloudflare · Daniel Tinizaray

El pipeline me dio luz verde y el edge de Cloudflare me dio HTML viejo. Tres assets nuevos respondían con el mismo archivo de 24,162 bytes y content-type: text/html. El deployment en Pages tenía los archivos correctos, así que el origen no era el problema: la cache del custom domain había guardado respuestas de un deploy anterior y no las soltaba. Esto es el post-mortem del diagnóstico y de la política de purge que ahora ejecuto en cada publicación.

Síntoma: mismo tamaño para tres assets distintos

Pasé por el flujo completo de edición: HTML nuevo, build local, validadores prebuild, deploy, verificación de las páginas en 200. Todo en orden hasta que revisé los downloads de imágenes por curl. Tres imágenes de héroe distintas, tres respuestas con exactamente el mismo tamaño en bytes. No es una coincidencia posible en imágenes reales: era el mismo cuerpo de error HTML repetido tres veces.

$ curl -sI https://danieltini.dev/blog/hero-nueva-1.jpg | head -6
HTTP/2 404
content-type: text/html; charset=utf-8
content-length: 24162
cf-cache-status: HIT

Dos datos en una sola línea cerraron el diagnóstico: content-type: text/html junto a cf-cache-status: HIT. El edge tenía una respuesta 404 guardada de antes de que los assets existieran, y la servía sin volver a preguntarle al deployment. El HTML de las páginas sí cargaba, así que el sitio "funcionaba" para cualquier visitante que no mirara más allá del primer scroll. Ese es el peligro de un 404 cacheado: no rompe el pipeline, no rompe el deploy, solo corrompe silenciosamente lo que el usuario ve.

Confirmando: dos orígenes, dos respuestas distintas

Con el custom domain y el deployment de Pages expuestos por URLs separadas, la comparación fue directa. La misma imagen pedido en ambas rutas devolvía resultados opuestos:

# Preview directo del deployment en Pages
$ curl -sI https://danieltini-dev.pages.dev/blog/hero-nueva-1.jpg | head -4
HTTP/2 200
content-type: image/jpeg
content-length: 458000

# Custom domain productivo
$ curl -sI https://danieltini.dev/blog/hero-nueva-1.jpg | head -6
HTTP/2 404
content-type: text/html
content-length: 24162

El asset existía y era correcto en el deployment; la respuesta rancía vivía solo en la cache del edge. Comprobé también que un asset viejo, publicado semanas antes, se servía perfecto en el mismo dominio. Si el problema fuera el deployment o la configuración del hostname, fallaría todo. Solo fallaba lo que el edge ya había decidido no revalidar. Corroboré la edad de la respuesta con el header Age, que daba segundos en el rango de horas, y cesé de dudar.

El fix: purge dirigido, no purge_everything

Tenía dos caminos. El martillo, purge_everything, invalida la zona completa y baja el hit ratio de todas las URLs a cero por nada. El bisturí, purge por URL explícita, toca solo lo sospechoso. Elegí el bisturí con la API de Cloudflare:

$ export ZONE_ID=cac97e32eea03d3786b3b4a7aa25b67f
$ curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"files":[
    "https://danieltini.dev/blog/hero-nueva-1.jpg",
    "https://danieltini.dev/blog/hero-nueva-2.jpg",
    "https://danieltini.dev/blog/hero-nueva-3.jpg",
    "https://danieltini.dev/blog/",
    "https://danieltini.dev/"
  ]}'

{"result":{},"success":true,"errors":[]}

Nótese el detalle distinto respecto al incidente original: no sospeché solo las tres imágenes. Agregué la URL del índice del blog y la home, porque los cards de esas páginas referencian los mêmes assets y estaban ellos mismos en riesgo de servir HTML con referencias rotas. El patrón que uso desde ese día es purge dirigido por URL + índice + home: las URLs cambiadas más las páginas que las contienen.

Incluí el índice a mano en el purge también por otro caso que me tocó depurar después: un deploy que cambió el path de un post dejaba un 301 viejo en el cache del Worker y en el cache de *.pages.dev, una capa que el purge de la zona del apex no alcanza a invalidar. Ahí aprendí que el purge de zona solo cubre el custom domain, no el subdominio gestionado por Cloudflare Pages. Por eso el purge dirigido es el primer paso, y la verificación con cf-cache-status es el segundo.

Verificación post-deploy: contracto, no esperanza

El error que cometí no fue el cache. Fue confiar en un deploy verde sin verificar qué devuelve el edge. Ahora tengo un contracto post-deploy de dos pasos: status + content-type + tamaño, y estado del edge inmediato. Si un asset recién deployeado responde 404 con cf-cache-status: HIT, no pienso: purge por URL ya.

#!/bin/bash
# edge-check.sh — uso tras cada deploy de Cloudflare Pages
for url in "$@"; do
  echo "== $url"
  curl -s -o /dev/null -w "%{http_code} %{content_type} %{size_download}\n" "$url"
  curl -sI "$url" | grep -iE "cf-cache-status|^age:"
done

El script es intencionalmente corto. Lo que importa no es el tool, es la lista de señales: un 404 con content-type HTML y cf-cache-status HIT es cache rancio, no pipeline roto. Un 404 con cf-cache-status: MISS o DYNAMIC sí es señal del deployment. Distinguir los dos casos en un solo curl me ahorró una hora de debug la siguiente vez que pasó, y pasó de nuevo.

Resultados

Lo que dejó el incidente, medido sobre mi propio flujo de publicación:

  • 3 assets afectados, todos con el mismo cuerpo de 24,162 bytes de HTML cacheado como 404 por el edge del custom domain.
  • Tiempo de diagnóstico: ~15 minutos con curl y cf-cache-status. Sin esas señales habré culpado al build y habré corrido dos deploys de más.
  • Recuperación post-purge: las tres imágenes devolvían content-type: image/jpeg con sus 458 KB reales en menos de 60 segundos, sin bajar el hit ratio del resto de la zona.
  • Política adoptada: escape de purge_everything, purge dirigido por URL + index + home en cada deploy, e integración del check del edge al flujo de publicación.

Más de cómo opero esta infraestructura —CI/CD, cache del edge, Cloudflare Pages— en danieltini.dev y en mi GitHub.


¿Te gustó este artículo?

Si estás lidiando con estos desafíos en tu empresa, hablemos. Sin compromiso. 30 minutos para entender tu situación.

Agenda una llamada gratuita →