HTML viejo en el edge: por qué mi deploy de Cloudflare no se veía · Daniel Tinizaray

HTML viejo en el edge: por qué mi deploy de Cloudflare no se veía · Daniel Tinizaray

Publiqué tres imágenes de héroe nuevas en este blog. El build terminó en verde, el deploy subió el directorio completo y el preview en *.pages.dev se veía perfecto. Al abrir el dominio productivo, las imágenes devolvían una página HTML de 24KB en lugar del JPG. El origen estaba bien: lo rancio estaba en el edge de Cloudflare. Este es el post-mortem del diagnóstico y de la regla post-deploy que adopté.

Síntoma: assets nuevos que responden como HTML

El flujo fue el estándar que uso para este sitio: generar las imágenes, copiarlas a los assets, build, verificar y deploy con wrangler pages deploy. La verificación post-deploy reportó 200 en las páginas. Luego un detalle me hizo sospechar: al pedir los tres JPG nuevos por curl, todos respondían con exactamente el mismo tamaño, 24162 bytes. Tres imágenes distintas, un solo tamaño idéntico. Eso no es una imagen: es error HTML, casi seguro el mismo cuerpo repetido.

$ 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

content-type: text/html y cf-cache-status: HIT en la misma respuesta: el edge estaba sirviendo una respuesta 404 cacheada, no consultando el origen. Lo peor del caso es que el HTML de las páginas sí cargaba bien, así que el sitio "funcionaba": las imágenes simplemente no aparecían. Un 404 visible grita; un 404 cacheado susurra.

Diagnóstico: comparar dos orígenes, no uno

La primera prueba delataron la diferencia entre el asset origin y el edge cache. Pedí la misma imagen en las dos rutas que Cloudflare expone:

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

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

Misma imagen, dos respuestas distintas. El deployment en Pages tenía el asset intacto; el hostname productivo devolvía una respuesta rancio. Con eso quedaba claro que el problema no estaba en el build ni en el deploy: estaba en la cache asociada al custom domain, que había aprendido la URL cuando el asset no existía todavía y se la quedó.

El detalle que confirmó el diagnóstico: un asset viejo (hero-finops.jpg, publicado semanas antes) se servía perfecto en el mismo dominio. Si el hostname estuviera roto o el deployment corrupto, fallaría todo; solo fallaba lo que el edge ya había cachedo como 404 antes de que el deploy subiera los archivos.

El fix: purge dirigido por URL

Dos caminos posibles: purge_everything (el martillo) o purge dirigido (el bisturí). Elegí el bisturí: tres URLs explícitas por la API de Cloudflare.

$ export ZONE_ID=$(cat zone_id.txt)
$ 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"
  ]}'

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

Nunca uso purge_everything por defecto: invalida el cache de toda la zona, seconda el hit ratio de为零 las URLs que no tienen nada que ver con el release y carga el origen de gratis. El purge dirigido por URL toca exactamente lo sospechoso. En 60 segundos, las tres imágenes volvieron a responder content-type: image/jpeg con sus 458KB reales.

La regla que adopté: observabilidad del edge post-deploy

El incidente dejó una lección de arquitectura: un release en Cloudflare Pages tiene dos orígenes que verificar, el deployment (Pages) y la cache del edge (custom domain). Verificar solo contra pages.dev valida el origen equivocado. Agregué al flujo un check post-deploy que mira status, content-type, tamaño y estado del edge:

#!/bin/bash
# post-deploy-edge-check.sh — verificación tras cada deploy
URLS="$@"
for url in $URLS; do
  # status + content-type + bytes: un 404 HTML no pasa este check
  curl -s -o /dev/null \
    -w "%{http_code} %{content_type} %{size_download}\n" "$url"
  # estado inmediato del edge: HIT en una URL recién publicada
  # es señal directa de cache rancio → purge dirigido
  curl -sI "$url" | grep -iE "cf-cache-status|^age:"
done

Con ese output ya no hay debate: si un asset recién deployeado responde 404 con cf-cache-status: HIT, no hay que pensar dos horas — es purge por URL ahora. Si responde 200 con content-length coherente, release bueno.

Lo que aprendí

  • Un 404 cacheado es peor que un error visible: tres imágenes cacheadas como HTML de 24162 bytes rompieron el sitio sin romper el pipeline. El 404 en vivo al menos se ve; el cacheado se queda.
  • El deploy no invalida todo lo que crees que invalida: el deployment en Pages estaba perfecto y el edge guardaba la respuesta de antes. Depurar el cache de Cloudflare es un paso separado, no un hechoData del deploy.
  • El purge se automatiza, no se improvisa: el purge dirigido (URLs + index + home) quedó integrado al flujo de publicación. Esperar a que "se refresque solo" fue el costo real: el incidente me llevó ~15 minutos de diagnóstico que la verificación de edge habría evitado.
  • Métricas, no sensaciones: los tres signals — content-type, content-length, cf-cache-status — distinguen deploy roto de cache rancio en un solo curl. Sin ellos habría culpado al build.

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 →